Kotlin 协程设计思想(五):协程异常为什么这么难理解?
—— 从 launch、async、SupervisorJob 到 CoroutineExceptionHandler,彻底讲透 Kotlin 协程异常传播机制
前面四篇
Kotlin 协程设计思想(一):CoroutineContext 到底是什么?为什么 Job 和 Dispatcher 可以直接相加?-CSDN博客
Kotlin 协程设计思想(二):Job 到底是什么?为什么协程能被取消?-CSDN博客
Kotlin 协程设计思想(三):Dispatchers 到底是什么?切线程真的只是切线程吗?-CSDN博客Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?-CSDN博客
我们已经讲了:
CoroutineContext
↓
Job
↓
Dispatcher
↓
launch / async / withContext
本来以为协程已经学得差不多了。
结果真正做项目的时候,最容易把人搞懵的却是:
异常处理
例如:
try-catch
有时候能捕获:
launch {
}
里面的异常。
有时候:
又捕获不到
例如:
CoroutineExceptionHandler
有时候生效:
有时候又不生效
再比如:
async
明明已经抛异常了。
结果:
程序没崩
日志没打印
Handler没收到
直到:
await()
才突然爆出来。
于是很多人开始觉得:
协程异常处理太诡异了
其实并不是。
只是你还没有理解:
Kotlin协程的异常传播模型
一、先记住一句话
如果让我一句话概括协程异常:
异常永远沿着 Job 树向上传播。
这句话能解释:
90%
协程异常问题
二、先看 launch
例如:
viewModelScope.launch {
throw RuntimeException("error")
}
会发生什么?
很多人以为:
异常在当前协程结束
其实不是。
结构:
ViewModelScope
│
└── launch
异常:
launch
↓
ViewModelScope
向上传播。
如果父协程没有处理:
整个作用域取消
三、为什么 launch 的异常会直接崩?
因为:
launch
返回:
Job
没有结果。
所以:
异常不能藏起来
只能:
立即上报父协程
这也是:
launch {
throw RuntimeException()
}
容易直接看到异常的原因。
四、再看 async
例如:
val deferred = async {
throw RuntimeException("error")
}
很多人第一次都会懵:
怎么没崩?
因为:
async
返回:
Deferred<T>
而:
Deferred
本来就是拿结果的
于是 Kotlin 设计者认为:
异常也是结果的一部分
所以:
async 不立即上报异常
而是:
先存起来
放进:
Deferred
里面。
五、await 才是真正的爆点
例如:
val deferred = async {
throw RuntimeException("error")
}
delay(5000)
此时:
不会抛异常
直到:
deferred.await()
执行。
异常才真正出现:
try {
deferred.await()
} catch (e: Exception) {
}
所以:
launch
立即传播异常
async
延迟传播异常
六、为什么这样设计?
因为:
async
的设计目标是:
并发计算
例如:
val user = async {
}
val order = async {
}
val banner = async {
}
最终:
await()
时统一拿结果。
如果:
其中一个异常
立即上报
那么:
并发模型就乱了
所以:
async
必须把异常缓存起来
等:
await()
统一处理。
七、CoroutineExceptionHandler 为什么总感觉不生效?
这是面试高频。
很多人:
val handler =
CoroutineExceptionHandler { _, e ->
Log.e("TAG", e.message ?: "")
}
然后:
scope.launch(handler) {
}
没问题。
但是:
scope.async(handler) {
}
发现:
Handler没回调
原因:
async的异常
已经被Deferred接管
不会立即传播。
所以:
CoroutineExceptionHandler
处理不了async内部异常
必须:
await()
时处理。
这是无数人踩过的坑。
八、为什么 try-catch 有时候不生效?
例如:
try {
launch {
throw RuntimeException()
}
} catch (e: Exception) {
}
很多人觉得:
应该捕获
实际上:
捕获不到
为什么?
因为:
launch {
}
已经开新协程了。
结构:
当前协程
│
└── launch
异常发生在:
launch子协程
里面。
而:
try-catch
只包住:
当前协程
所以:
根本捕获不到
九、什么时候 try-catch 能捕获?
例如:
launch {
try {
throw RuntimeException()
} catch (e: Exception) {
}
}
这里:
异常发生地
=
捕获地
当然能捕获。
或者:
val deferred = async {
throw RuntimeException()
}
try {
deferred.await()
} catch (e: Exception) {
}
也能捕获。
因为:
异常最终在await抛出
十、协程异常为什么会导致整个作用域取消?
回到上一篇:
Job树
例如:
Parent
│
├── Child1
│
├── Child2
│
└── Child3
Child1:
抛异常
普通 Job:
Child1异常
↓
Parent取消
↓
Child2取消
↓
Child3取消
于是:
全家陪葬
十一、SupervisorJob 为什么出现?
Google发现:
很多业务场景不合理。
例如:
首页加载
包含:
用户信息
Banner
推荐商品
Banner失败:
为什么用户信息也没了?
不合理。
于是:
SupervisorJob()
出现。
十二、SupervisorJob 的异常传播
结构:
Parent
│
├── Child1 崩
│
├── Child2 正常
│
└── Child3 正常
此时:
Child1取消
不会:
影响兄弟节点
所以:
SupervisorJob
=
异常隔离器
十三、coroutineScope 与 supervisorScope
又是经典面试题。
coroutineScope
结构:
一家人
一个孩子异常:
全家取消
例如:
coroutineScope {
async {
}
async {
}
}
supervisorScope
结构:
兄弟独立
一个孩子异常:
其它继续
例如:
supervisorScope {
async {
}
async {
}
}
这其实和:
Job
SupervisorJob
是一脉相承的。
十四、项目里的最佳实践
对于:
launch
推荐:
launch {
try {
} catch (e: Exception) {
}
}
对于:
async
推荐:
runCatching {
deferred.await()
}
或者:
try {
deferred.await()
} catch (e: Exception) {
}
对于:
多个并发请求
推荐:
supervisorScope
避免:
一处失败
全局崩盘
十五、终于串起来了
现在回头看:
CoroutineContext
↓
Job
↓
Dispatcher
↓
launch
↓
async
↓
Exception
其实全是一条线。
异常传播:
不是魔法
而是:
Job树
上的传播规则。
十六、最终总结
如果让我一句话解释:
launch
的异常:
立即向父协程传播。
如果让我解释:
async
的异常:
先缓存到Deferred
await时再抛出。
如果让我解释:
CoroutineExceptionHandler
为什么经常不生效:
因为async的异常根本没传播出来。
如果让我解释:
SupervisorJob
存在的意义:
隔离异常传播。
真正理解协程异常,
本质上就是理解:
Job树
如何传播异常。
下篇预告
到这里:
CoroutineContext
✓
Job
✓
Dispatcher
✓
launch/async
✓
Exception
✓
基本串起来了。
那么最后一个问题来了:
为什么Google一直强调:
Structured Concurrency(结构化并发)?
为什么不推荐GlobalScope?
为什么CoroutineScope这么重要?
下一篇我们继续:
《Kotlin 协程设计思想(六):结构化并发到底是什么?为什么 Google 一直强调 Scope?》
从 GlobalScope、CoroutineScope、LifecycleScope 到 ViewModelScope,
彻底讲透 Kotlin 协程最核心的设计哲学。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)