第27章:协程异常处理

协程的异常处理,说实话,是很多Kotlin开发者容易踩坑的地方。我自己刚接触协程那会儿,就天真地以为try-catch能搞定一切。结果呢?线上出了个崩溃,查了半天才发现——协程里的异常传播机制,跟普通线程完全不是一回事。

这一章,我就带你彻底搞懂协程异常处理的三种武器:try-catchCoroutineExceptionHandlersupervisorScope。它们各自适合什么场景?怎么搭配使用?咱们一个一个说。

27.1 协程中的try-catch:最基础,但别乱用

先看个最简单的例子。你在协程里做网络请求,可能抛出IOException:

viewModelScope.launch {
    try {
        val result = api.fetchData()
        // 处理结果
    } catch (e: IOException) {
        // 网络异常处理
        showError("网络不给力")
    }
}

这样写没问题。但注意——try-catch只能捕获它所在协程体内的异常。如果你在launch外面包try-catch,那就没用了。为什么?因为launch是异步的,异常发生时,外层代码早就执行完了。

⚠️ 注意: try-catch 无法捕获子协程抛出的异常。子协程的异常会沿着协程树向上传播,直到被父协程捕获或导致崩溃。

举个例子你就明白了:

// 错误示范:这样捕获不到子协程的异常
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 是「兜底」用的,不是「替代」try-catch的。它适合记录日志、上报崩溃,不适合做业务上的异常恢复。

还有一点: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 + CoroutineExceptionHandler,就能实现「子协程独立失败,且异常统一上报」的效果。我在做多接口并发请求时,经常这么搭配。

我曾经在一个项目中,需要同时拉取用户信息、商品列表、广告数据。三个接口互相独立,任何一个失败都不影响其他。用supervisorScope就完美解决了。如果当时用默认的coroutineScope,一个接口挂了,其他两个也会被取消,用户体验就很差。

27.4 三种方案的对比与选择

为了方便你快速决策,我整理了一张对比表:

方案 适用场景 特点 注意事项
try-catch 单个协程内的可预期异常 精确控制,代码直观 无法捕获子协程异常
CoroutineExceptionHandler 全局兜底、日志上报 统一处理未捕获异常 对async无效,需配合launch使用
supervisorScope 子协程独立失败、并发互不影响 异常不向上传播 异常会传递到顶层Handler

27.5 知识体系图

下面这张图,帮你理清协程异常处理的整体脉络:

协程异常处理知识体系 协程异常处理 try-catch CoroutineExceptionHandler supervisorScope 特性 • 捕获协程体内的异常 • 无法捕获子协程异常 • 适合可预期的业务异常 • 代码直观,精确控制 特性 • 全局兜底处理 • 对 async 无效 • 适合日志上报 • 需配合 launch 使用 特性 • 子协程独立失败 • 异常不向上传播 • 适合并发互不影响 • 异常到顶层Handler

27.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要用try-catch包launch——我之前就这么干过,以为能捕获子协程异常,结果代码静默失败,排查了半天。记住,try-catch要写在launch里面。
  • CoroutineExceptionHandler 要放在顶层——如果你在子协程里设置Handler,父协程的异常可能不会被捕获。我建议只在根协程或Application级别设置。
  • supervisorScope 不是万能的——它只阻止异常向上传播,但异常本身还是会被抛出。如果你不处理,它最终会到达全局的异常处理器。所以,该用try-catch的地方还是得用。

嗯,协程异常处理就这些了。说白了,就是搞清楚「异常往哪传」和「谁来接」。try-catch是精确打击,CoroutineExceptionHandler是防空导弹,supervisorScope是防弹衣。选对武器,你的协程代码就能稳如老狗。


公众号:蓝海资料掘金营,微信 deep3321