之前我写过一篇文章 Kotlin协程异常一文通,那篇文章直接从异常的函数入手,讲述了 Kotlin 协程的特点和部分用法。
我把那篇文章分享给了同事,过了一段时间,他还是来问我:coroutineScope 处理异常有什么特点?
虽然那篇文章我认为写的非常明确,但是我想他既然记不住,可能太教科书了。我告诉他,你等我下篇文章,我给你讲个明明白白。
于是,我打算从案例入手,来几条黄金准则,一篇文章搞懂在 Kotlin 协程中如何处理异常。
协程异常处理的核心机制是结构化并发。
这是 Kotlin 协程的设计原则,也是为什么协程异常有点难处理的原因。
你可以把它想象成一棵任务树:只要某个子协程因异常失败,就会立即通知父协程;父协程会马上取消所有其他子协程,随后自身也被取消,并将异常继续向上传播。
这种机制能确保不会有任何“孤儿协程”被遗漏,让协程的生命周期始终处于可控状态。
下面我们结合实际开发场景,拆解协程异常处理的核心逻辑和正确姿势。
场景一:使用 launch
launch 是典型的 “fire-and-forget(发起即忘)”型协程构建器,适合不需要返回结果的任务。但它的异常处理逻辑很容易踩坑,新手常在这里栽跟头。
错误做法:在 launch 外层套 try-catch
这是高频错误用法:把 launch 调用包裹在 try-catch 里,这样做根本无法捕获协程内部抛出的异常。
问题原因
launch 会立即返回,而协程体里的代码是在后台线程异步执行的。等异常真正抛出时,外层的 try-catch 早就执行完了。最终异常会一路冒泡到顶层异常处理器,若无人处理,应用就会直接崩溃。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Before launch")
try {
// 这个 launch 块会抛出异常
launch {
println("Inside launch: Throwing exception...")
delay(500) // 模拟耗时操作
throw RuntimeException("Something went wrong!")
}
} catch (e: Exception) {
// ❌ 这段代码永远不会执行 ❌
println("Caught exception: $e")
}
println("After launch")
delay(1000) // 保持主线程存活,观察崩溃现象
println("Main finished")
}
Before launch
After launch
Inside launch: Throwing exception...
Exception in thread "main" java.lang.RuntimeException: Something went wrong!
...
可以看到,“Caught exception” 从未打印,程序直接崩溃。
正确做法:在 launch 内部使用 try-catch
要处理某个 launch 协程的异常,必须把 try-catch 直接放在协程的 lambda 表达式内部。
这个做法简直是异常处理的古法捕获!
这样能让异常处理逻辑紧跟异常发生的位置——也就是协程自身的执行路径上。只有这样,才能优雅处理错误,避免整个作用域崩溃。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Before launch")
launch {
try {
// 风险代码放在 try 块中
println("Inside launch: Doing some work...")
delay(500)
throw RuntimeException("Oops, failed!")
} catch (e: Exception) {
// ✅ 正确捕获异常
println("Caught exception inside launch: ${e.message}")
}
}
println("After launch")
delay(1000) // 等待 launch 执行完成
println("Main finished")
}
Before launch
After launch
Inside launch: Doing some work...
Caught exception inside launch: Oops, failed!
Main finished
此时程序能正常处理异常,不会崩溃。
场景二:使用 async 构建器
当需要执行任务并后续获取结果时,用 async 构建器,它会返回一个 Deferred 对象。async 的异常处理逻辑和 launch 不同——它会“暂存”异常,直到你调用 await() 获取结果时才会抛出。
错误做法:在 async 调用周围套 try-catch
仅把 async 本身包裹在 try-catch 里是无效的。
问题原因
async 会立即返回 Deferred 对象,协程内部的异常会被存储在这个对象中,等待你显式调用 await() 时才会暴露。因此,在 async 外层 try-catch 根本捕获不到这个延迟出现的异常。
正确做法:在 await() 调用周围用 try-catch
只有调用 await() 获取结果时,async 内部的异常才会被重新抛出——这是异常真正“显现”的时刻。
这种设计让你可以自主决定处理失败的时机和方式。异常是“延迟结果”的一部分,只有主动请求结果时才会触发异常。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Before async")
val deferredResult: Deferred<String> = async {
println("Inside async: About to fail...")
delay(500)
throw IllegalStateException("Async operation failed!")
"This will never be returned"
}
println("After async")
try {
// 异常在调用 await() 时抛出
val result = deferredResult.await()
println("Result: $result")
} catch (e: Exception) {
// ✅ 正确捕获异常
println("Caught exception on await: ${e.message}")
}
println("Main finished")
}
Before async
After async
Inside async: About to fail...
Caught exception on await: Async operation failed!
Main finished
Exception in thread "main" java.lang.IllegalStateException: Async operation failed!
... (堆栈信息) ...
注意:虽然 catch 块成功捕获了异常,但由于顶层异常未处理,程序仍会崩溃。
这里正好体现了协程异常向上传播的特性。
若要完全避免崩溃,需在顶层添加异常处理逻辑,或使用 CoroutineScope 的异常处理器。
或者,你继续往下看!
替代方案:在 async 内部使用 try-catch
在 async 内部处理异常,能实现错误本地化,并可根据处理逻辑从 catch 块返回结果。这种情况下,Deferred 对象返回后不会再抛出异常——因为异常已经提前处理完毕。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Before async")
val deferredResult = async {
try {
println("Inside async: About to fail...")
delay(500)
throw IllegalStateException("Async operation failed!")
} catch (e: Exception) {
println("Caught exception inside async: ${e.message}")
return@async "Fallback result" // 异常处理后返回默认结果
}
"This will never be returned"
}
println("After async")
val result = deferredResult.await()
println("Result: $result")
println("Main finished")
}
Before async
After async
Inside async: About to fail...
Caught exception inside async: Async operation failed!
Result: Fallback result
Main finished
此时程序不会崩溃,因为异常已在 async 内部处理完毕,await() 能正常获取到 catch 块返回的结果。
场景三:父子协程关系
这是结构化并发的核心价值所在。coroutineScope 会等待所有子协程完成,一旦任意一个子协程失败,整个作用域会立即取消其他所有子协程,随后自身也失败。
一个子协程失败,全作用域取消
这种机制能防止程序进入不一致状态。如果某个关键操作失败,取消整个关联操作远比让其他部分继续运行更安全。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Starting the scope...")
try {
coroutineScope { // 创建新作用域
launch {
try {
println("Child 1: Working for 1000ms...")
delay(1000)
println("Child 1: Finished.") // 不会执行
} finally {
println("Child 1: I was cancelled!")
}
}
launch {
println("Child 2: Working for 500ms then failing...")
delay(500)
throw RuntimeException("Child 2 failed!")
}
}
} catch (e: Exception) {
// Child 2 的异常向上传播到父作用域并被捕获
println("Caught exception in parent scope: ${e.message}")
}
println("Scope finished.")
}
Starting the scope...
Child 1: Working for 1000ms...
Child 2: Working for 500ms then failing...
Child 1: I was cancelled!
Caught exception in parent scope: Child 2 failed!
Scope finished.
可以看到,Child 2 失败后,Child 1 还没完成工作就被立即取消了。
嵌套作用域的异常传播
在协程作用域内嵌套另一个 coroutineScope 时,内层作用域会遵循同样的“失败即取消”规则。
如果父作用域失败,内层作用域会被取消;反之,内层作用域的任意子协程失败,也会导致内层所有子协程取消,并将异常传播到父作用域。这种嵌套结构确保了整个任务树的一致性和可控性。
fun main() = runBlocking {
println("Starting the scope...")
launch {
println("Parent: Working for 500ms then failing...")
delay(500)
// 抛出一个上层异常
throw RuntimeException("Parent failed!")
}
try {
coroutineScope { // 创建新作用域
launch {
try {
println("Child 1: Working for 1000ms...")
delay(1000)
println("Child 1: Finished.") // 不会执行
} finally {
println("Child 1: I was cancelled!")
}
}
}
} catch (e: Exception) {
// 上层异常导致协程取消并被捕获
println("Caught exception in scope: ${e.message}")
}
}
Starting the scope...
Parent: Working for 500ms then failing...
Child 1: Working for 1000ms...
Child 1: I was cancelled!
Caught exception in scope: Parent job is Cancelling
...
场景四:隔离
如果希望某个子协程的失败不影响其他兄弟协程,就用 supervisorScope——它会覆盖父作用域的取消策略。
使用思路
supervisorScope 中直接子协程的异常,不会触发父作用域或兄弟协程的取消。这种特性在 UI 应用中特别实用,比如多个独立任务并行运行时,一个任务出错不该导致整个界面卡死。
重要提示:监督机制仅对直接子协程生效。
这一点我发现很多开发者包括我的同事也忽略了!你可以这样记住:
爸爸只管儿子,管不着孙子!
孙子谁管?儿子管!
当然,如果在 supervisorScope 内部嵌套 coroutineScope,这个内层作用域仍遵循“失败即取消”规则——其内部任意子协程失败,都会导致内层所有子协程被取消。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Starting the supervisor scope...")
try {
supervisorScope { // 子协程失败隔离
launch {
println("Child 1: Working for 500ms then failing...")
delay(500)
throw RuntimeException("Child 1 failed!")
}
launch {
try {
println("Child 2: Working for 1000ms...")
delay(1000)
println("Child 2: Finished successfully!") // 会执行
} finally {
println("Child 2: I was NOT cancelled!")
}
}
}
} catch (e: Exception) {
// ❌ 不会执行,supervisorScope 不传播子协程异常
println("Caught exception in parent scope: $e")
}
println("Supervisor scope finished.")
}
Starting the supervisor scope...
Child 1: Working for 500ms then failing...
Child 2: Working for 1000ms...
Exception in thread "main" java.lang.RuntimeException: Child 1 failed!
... (堆栈信息) ...
Child 2: Finished successfully!
Child 2: I was NOT cancelled!
Supervisor scope finished.
这里程序依然崩溃了——原因是 supervisorScope 只阻止取消传播(也就是不会取消其他儿子的任务),不会自动处理异常。
Child 1 抛出的异常未被捕获,最终导致主线程崩溃。因此,必须在 supervisorScope 的子协程内部手动处理异常。
正确使用 supervisorScope
在 supervisorScope 的子协程内部添加 try-catch,手动处理异常。
再次用到古法捕获!你不要管老不老,关键是好使!
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Starting the supervisor scope...")
supervisorScope {
// Child 1 自行处理异常
launch {
try {
println("Child 1: I'm going to fail.")
delay(500)
throw RuntimeException("Child 1 failed!")
} catch (e: Exception) {
println("Caught in Child 1: ${e.message}")
}
}
// Child 2 独立运行,不受影响
launch {
println("Child 2: I will succeed.")
delay(1000)
println("Child 2: Finished successfully!")
}
}
println("Supervisor scope finished.")
}
Starting the supervisor scope...
Child 1: I'm going to fail.
Child 2: I will succeed.
Caught in Child 1: Child 1 failed!
Child 2: Finished successfully!
Supervisor scope finished.
此时运行正常:Child 1 失败后自行处理了错误,Child 2 不受影响并顺利完成任务,同时也不会影响程序运行,一切照常!
场景五:全局异常捕获
CoroutineExceptionHandler 是协程异常处理的“最后防线”,它是一个特殊的上下文元素,可添加到顶层作用域,用于捕获所有未被其他方式处理的异常。
适用场景
主要用于日志记录、错误上报或清理未处理异常,特别适合与 GlobalScope 或 SupervisorJob 配合使用。
无效场景
对普通 coroutineScope 的子协程无效——因为 coroutineScope 会在子协程失败时主动取消并处理异常,不会让异常冒泡到顶层处理器。
但对 async 有效,因为其异常被封装在 Deferred 中,直到 await() 才暴露。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 1. 创建异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught by CoroutineExceptionHandler: $exception")
}
// 2. 创建带处理器的 SupervisorJob 作用域
val scope = CoroutineScope(SupervisorJob() + handler)
// 3. 启动会失败的协程
scope.launch {
println("Child 1: Failing...")
throw AssertionError("Something is wrong!")
}
// 4. 启动不受影响的协程
scope.launch {
delay(500)
println("Child 2: I'm alive!")
}
// 等待协程执行
delay(1000)
println("Main finished.")
}
Child 1: Failing...
Caught by CoroutineExceptionHandler: java.lang.AssertionError: Something is wrong!
Child 2: I'm alive!
Main finished.
处理器成功捕获了 Child 1 的异常,同时 Child 2 不受影响正常运行,且程序也正常运行,没有崩溃——这正是 SupervisorJob 隔离失败的特性与 CoroutineExceptionHandler 全局捕获的结合效果。
场景六:supervisorScope 中使用 async
supervisorScope 隔离失败的规则对 async 同样适用:某个 async 子协程失败,不会影响其他兄弟协程(无论 launch 还是 async)。
但 async 的异常仍会暂存于 Deferred 对象中,需在调用 await() 时手动处理。
使用思路
这种组合适合多个独立的异步任务并行执行的场景。即使某个任务失败,其他任务仍能正常完成,且失败任务的异常可在获取结果时单独处理。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Starting supervisor scope with async...")
supervisorScope {
// 会失败的 async 任务
val deferredFailure = async {
println("Async 1: I will fail in 500ms.")
delay(500)
throw IllegalStateException("Failure!")
}
// 会成功的 async 任务
val deferredSuccess = async {
println("Async 2: I will succeed in 1000ms.")
delay(1000)
"Success!"
}
// 处理失败任务的异常
try {
deferredFailure.await()
} catch (e: Exception) {
println("Caught expected failure from Async 1: ${e.message}")
}
// 获取成功任务的结果
try {
val result = deferredSuccess.await()
println("Result from Async 2: $result")
} catch (e: Exception) {
println("Caught unexpected failure from Async 2: $e")
}
}
println("Supervisor scope finished.")
}
Starting supervisor scope with async...
Async 1: I will fail in 500ms.
Async 2: I will succeed in 1000ms.
Caught expected failure from Async 1: Failure!
Result from Async 2: Success!
Supervisor scope finished.
可以看到,deferredSuccess 未受 deferredFailure 异常影响,正常完成并返回结果;同时,deferredFailure 的异常在 await() 时被成功捕获。
场景七:取消是一种特殊的异常
协程被取消时,会抛出 CancellationException——这是一种特殊异常,通常会被协程机制自动忽略,无需手动捕获。
使用思路
CancellationException 代表协程正常取消,是结构化并发的常规行为。虽然可以用 try-catch 捕获它(比如用于日志记录),但不应“吞掉”它——若捕获后需要执行逻辑,务必重新抛出,确保取消流程完整。协程的清理逻辑(如关闭资源、释放连接),最佳位置是 finally 块。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
println("Job: I'm working...")
delay(2000) // 挂起点,可响应取消
println("Job: I'm done.") // 不会执行
} catch (e: CancellationException) {
// 日志记录,必须重新抛出
println("Job: I was cancelled. Re-throwing exception.")
throw e
} catch (e: Exception) {
println("Job: Caught some other exception: $e")
} finally {
// ✅ 清理逻辑的正确位置
println("Job: Finally block executed for cleanup.")
}
}
delay(1000)
println("Main: I'm tired of waiting, cancelling the job.")
job.cancelAndJoin() // 取消并等待协程完成
println("Main: Job has been cancelled.")
}
Job: I'm working...
Main: I'm tired of waiting, cancelling the job.
Job: I was cancelled. Re-throwing exception.
Job: Finally block executed for cleanup.
Main: Job has been cancelled.
场景八:不可取消的清理
如果 finally 块中的清理逻辑包含挂起函数(比如写文件、网络请求),协程被取消后,这些挂起调用会立即抛出 CancellationException,导致清理逻辑无法完整执行。
解决方案
在 finally 块中,通过 withContext(NonCancellable) 切换上下文。
NonCancellable 能保证块内代码完整执行,不受协程取消状态影响。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
println("Job: Working...")
delay(2000)
} finally {
println("Job: Entering finally block.")
// 切换到 NonCancellable 上下文执行挂起清理逻辑
withContext(NonCancellable) {
println("Job: Starting a suspending cleanup that takes 500ms...")
delay(500) // 模拟耗时清理
println("Job: Crucial cleanup finished.")
}
}
}
delay(1000)
println("Main: Cancelling the job.")
job.cancelAndJoin()
println("Main: Job cancelled.")
}
Job: Working...
Main: Cancelling the job.
Job: Entering finally block.
Job: Starting a suspending cleanup that takes 500ms...
Job: Crucial cleanup finished.
Main: Job cancelled.
即使协程被取消,NonCancellable 块内的 delay(500) 仍能顺利执行,确保清理逻辑完成。
如果你需要在协程被取消之后执行一个清理任务,但是这个清理任务又是一个可挂起的函数,那么 NonCancellable 就再适合不过了。
场景九:嵌套作用域与异常传播细节
coroutineScope(失败即取消)和 supervisorScope(隔离失败)的规则,仅对直接子协程生效。
嵌套结构中,内层作用域的规则不会被外层覆盖——比如 supervisorScope 内的 coroutineScope,仍遵循“内部失败即全取消”。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 顶层 supervisorScope:隔离直接子协程失败
supervisorScope {
// 直接子协程 1:正常运行
launch {
println("Supervisor's Child 1: I survived!")
}
// 直接子协程 2:包含内层 coroutineScope
launch {
coroutineScope { // 内层遵循“失败即取消”
println("Inner Scope Child: I'm about to fail!")
delay(500)
throw RuntimeException("Failure in inner scope")
}
}
}
println("All done.")
}
输出结果
Inner Scope Child: I'm about to fail!
Supervisor's Child 1: I survived!
Exception in thread "main" java.lang.RuntimeException: Failure in inner scope
...
关键结论:Supervisor's Child 1 未受影响(体现 supervisorScope 隔离特性);内层 coroutineScope 的子协程失败后,内层作用域自身被取消(体现 coroutineScope 规则);未捕获的异常最终导致程序崩溃。
场景十:任务层级详解
结构化并发的核心是“任务层级(Job Hierarchy)”,可以形象理解为一棵任务树:
-
在协程(或作用域)中启动新协程时,新协程会成为父协程的子任务;
-
父任务有两个核心责任:一是等待所有子任务完成后才会自身完成;二是若子任务失败(非
supervisor场景),则取消所有其他子任务并自身失败。
这种层级关系确保了协程的“结构化”,避免任务失控。
import kotlinx.coroutines.*
fun main() = runBlocking {
// runBlocking 是顶层父作用域
println("Parent Scope: I'm the parent job.")
val job1 = launch {
println("Child Job 1: I'm a child of the parent scope.")
delay(1000)
println("Child 1: I'm done.")
}
val job2 = launch {
println("Child Job 2: I'm also a child.")
delay(500)
// 创建孙协程(job2 的子协程)
launch {
println("Grandchild: My parent is Child 2.")
delay(500)
println("Grandchild: I'm done.")
}
println("Child 2: I'm done.")
}
println("Parent Scope: Waiting for my children to finish.")
// runBlocking 会等待所有子任务(job1、job2、孙协程)完成后才结束
}
输出结果
Parent Scope: I'm the parent job.
Parent Scope: Waiting for my children to finish.
Child Job 1: I'm a child of the parent scope.
Child Job 2: I'm also a child.
Grandchild: My parent is Child 2.
Grandchild: I'm done.
Child 2: I'm done.
Child 1: I'm done.
可见,runBlocking 作为父作用域,会等待所有子协程(包括嵌套的孙协程)完成后才结束,完全遵循任务层级的约束。
场景十一:supervisorScope vs CoroutineScope(SupervisorJob())
两者都基于 SupervisorJob 实现失败隔离,但适用场景不同:
-
supervisorScope { ... }:用于创建“局部隔离区”,适合隔离一组相关但独立的临时任务。它是一个挂起函数(这意味着它的运行需要一个协程域),会等待所有直接子协程完成;若子协程异常未被捕获,会抛出到外部作用域。
-
CoroutineScope(SupervisorJob()):创建一个可复用的作用域对象(
它本身就是在创建一个协程域),适合组件化架构(如 AndroidViewModel、独立模块)。作用域内的直接子协程相互隔离,且作用域不会自动结束,需显式调用cancel()销毁。
import kotlinx.coroutines.*
class Component {
// 创建带 SupervisorJob 和异常处理器的作用域
private val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, e ->
println("Component: Caught an error: $e")
})
// 启动高风险任务
fun startRiskyTask() {
scope.launch {
println("Risky Task: Starting...")
delay(1000)
throw RuntimeException("Something went wrong")
}
}
// 启动稳定任务
fun startStableTask() {
scope.launch {
println("Stable Task: I'm running...")
delay(1000)
println("Stable Task: I finished successfully.")
}
}
// 组件销毁时取消作用域
fun destroy() {
println("Component: Destroying and cancelling scope.")
scope.cancel()
}
}
fun main() = runBlocking {
val component = Component()
component.startRiskyTask()
component.startStableTask()
delay(2000) // 等待任务执行
component.destroy()
delay(500)
}
Risky Task: Starting...
Stable Task: I'm running...
Component: Caught an error: java.lang.RuntimeException: Something went wrong
Stable Task: I finished successfully.
Component: Destroying and cancelling scope.
即使高风险任务失败,稳定任务仍正常完成;作用域保持活跃直到显式销毁,符合组件化开发的生命周期管理需求。
场景十二:处理超时
长时间运行的任务可能导致资源占用或响应缓慢,协程提供了简洁的超时控制方案:
-
withTimeout(timeoutMillis) { ... }:执行代码块,若超时则抛出
TimeoutCancellationException,同时取消协程; -
withTimeoutOrNull(timeoutMillis) { ... }:非抛出版本,超时后返回
null,不会抛出异常,代码更简洁易维护。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 使用 withTimeout(抛出版)
try {
withTimeout(1000) {
println("Task 1: I have a second to complete.")
delay(2000) // 会超时
println("Task 1: I finished.") // 不会执行
}
} catch (e: TimeoutCancellationException) {
println("Task 1 failed: ${e.message}")
}
// 使用 withTimeoutOrNull(非抛出版)
val result = withTimeoutOrNull(1000) {
println("Task 2: Trying to complete in 1 second.")
delay(2000)
"Success"
}
if (result == null) {
println("Task 2 timed out and returned null.")
} else {
println("Task 2 finished with result: $result")
}
}
Task 1: I have a second to complete.
Task 1 failed: Timed out waiting for 1000 ms
Task 2: Trying to complete in 1 second.
Task 2 timed out and returned null.
实际开发中,withTimeoutOrNull 更常用,可避免额外的 try-catch 嵌套。
场景十三:等待多个异步任务的异常处理
当需要等待多个 async 任务完成时,推荐使用 awaitAll()——它遵循“全或无”原则,与 coroutineScope 逻辑一致:若任意一个任务失败,立即取消所有其他任务,并抛出第一个失败的异常。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Starting multiple async jobs.")
try {
coroutineScope {
val deferred1 = async {
delay(1000)
println("Job 1: Success.")
"Result 1"
}
val deferred2 = async {
delay(500)
println("Job 2: Failing!")
throw IllegalStateException("Job 2 Error")
}
// 等待所有任务完成,任意失败则立即抛出异常
val results = awaitAll(deferred1, deferred2)
println("All jobs finished: $results") // 不会执行
}
} catch (e: Exception) {
println("Caught exception in awaitAll: ${e.message}")
}
}
Starting multiple async jobs.
Job 2: Failing!
Caught exception in awaitAll: Job 2 Error
可以看到,Job 2 失败后,awaitAll() 立即抛出异常,Job 1 被取消(其延迟 1000ms 的任务未完成,因此“Job 1: Success.” 未打印)。
总结
以上场景覆盖了协程异常处理的核心场景和最佳实践。
屏幕前的你,现在强的可怕!
异常处理的核心原则是:利用结构化并发的层级特性,让异常处理逻辑紧跟异常发生位置,根据任务关联性选择合适的作用域(coroutineScope/supervisorScope),必要时用 CoroutineExceptionHandler 兜底。
实在不行,古法捕获!
掌握这些逻辑,就能让协程的异常处理既安全又优雅。
我把这篇文章甩给我的同事,我相信他以后绝对不会问我异常处理的问题了。