第27章:协程异常处理
协程的异常处理,说实话,是很多Kotlin开发者容易踩坑的地方。我自己刚接触协程那会儿,就天真地以为try-catch能搞定一切。结果呢?线上出了个崩溃,查了半天才发现——协程里的异常传播机制,跟普通线程完全不是一回事。
这一章,我就带你彻底搞懂协程异常处理的三种武器:try-catch、CoroutineExceptionHandler、supervisorScope。它们各自适合什么场景?怎么搭配使用?咱们一个一个说。
27.1 协程中的try-catch:最基础,但别乱用
先看个最简单的例子。你在协程里做网络请求,可能抛出IOException:
viewModelScope.launch {
try {
val result = api.fetchData()
// 处理结果
} catch (e: IOException) {
// 网络异常处理
showError("网络不给力")
}
}
这样写没问题。但注意——try-catch只能捕获它所在协程体内的异常。如果你在launch外面包try-catch,那就没用了。为什么?因为launch是异步的,异常发生时,外层代码早就执行完了。
举个例子你就明白了:
// 错误示范:这样捕获不到子协程的异常
try {
viewModelScope.launch {
launch {
throw RuntimeException("子协程挂了")
}
}
} catch (e: Exception) {
// 这里永远进不来
}
嗯,这里要注意:协程的结构化并发决定了异常会向上冒泡。子协程挂了,父协程也会跟着取消。这是设计上的取舍——要么一起成功,要么一起失败。
27.2 CoroutineExceptionHandler:全局兜底方案
那如果我想统一处理所有未捕获的异常呢?这时候就该CoroutineExceptionHandler登场了。
它有点像Thread的UncaughtExceptionHandler,但只对协程有效。我个人习惯在Application级别定义一个全局的异常处理器:
val globalExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("Coroutine", "未捕获的协程异常: ${throwable.message}")
// 上报到Bugly或Firebase
}
然后这样用:
viewModelScope.launch(globalExceptionHandler) {
// 可能会抛异常的代码
throw IllegalArgumentException("参数错误")
}
但注意——CoroutineExceptionHandler 只在未捕获的异常上生效。如果你已经在协程体内用try-catch处理了,那Handler就不会被调用。
还有一点:CoroutineExceptionHandler 对 async 无效。因为async的异常是通过await()传递的,你可以在调用处用try-catch捕获。这个区别很重要,我见过不少同事在这里翻车。
27.3 supervisorScope:让子协程独立失败
前面说了,协程的默认行为是「子协程失败,父协程跟着取消」。但有些场景下,我们希望子协程之间互不影响。比如同时请求三个接口,一个失败了,另外两个应该继续。
这时候就用supervisorScope。它创建了一个作用域,子协程的异常不会向上传播到父协程。
viewModelScope.launch {
supervisorScope {
launch {
delay(100)
throw RuntimeException("任务A失败")
}
launch {
delay(200)
println("任务B成功")
}
}
// 这里会继续执行,不会被上面的异常影响
println("supervisorScope 结束了")
}
运行结果:任务B成功,supervisorScope 结束。任务A的异常被吞掉了?不,它其实被传递到了顶层的CoroutineExceptionHandler。如果你没设置Handler,它会被打印到控制台,但不会导致整个作用域取消。
我曾经在一个项目中,需要同时拉取用户信息、商品列表、广告数据。三个接口互相独立,任何一个失败都不影响其他。用supervisorScope就完美解决了。如果当时用默认的coroutineScope,一个接口挂了,其他两个也会被取消,用户体验就很差。
27.4 三种方案的对比与选择
为了方便你快速决策,我整理了一张对比表:
| 方案 | 适用场景 | 特点 | 注意事项 |
|---|---|---|---|
| try-catch | 单个协程内的可预期异常 | 精确控制,代码直观 | 无法捕获子协程异常 |
| CoroutineExceptionHandler | 全局兜底、日志上报 | 统一处理未捕获异常 | 对async无效,需配合launch使用 |
| supervisorScope | 子协程独立失败、并发互不影响 | 异常不向上传播 | 异常会传递到顶层Handler |
27.5 知识体系图
下面这张图,帮你理清协程异常处理的整体脉络:
27.6 避坑指南
最后,分享几个我踩过的坑:
- 不要用try-catch包launch——我之前就这么干过,以为能捕获子协程异常,结果代码静默失败,排查了半天。记住,try-catch要写在launch里面。
- CoroutineExceptionHandler 要放在顶层——如果你在子协程里设置Handler,父协程的异常可能不会被捕获。我建议只在根协程或Application级别设置。
- supervisorScope 不是万能的——它只阻止异常向上传播,但异常本身还是会被抛出。如果你不处理,它最终会到达全局的异常处理器。所以,该用try-catch的地方还是得用。
嗯,协程异常处理就这些了。说白了,就是搞清楚「异常往哪传」和「谁来接」。try-catch是精确打击,CoroutineExceptionHandler是防空导弹,supervisorScope是防弹衣。选对武器,你的协程代码就能稳如老狗。
公众号:蓝海资料掘金营,微信 deep3321