Skip to content

异常处理与监督机制

协程的异常处理是很多初学者的噩梦。核心原则是:不要试图用 try-catch 包裹 launch,而是要在协程内部捕获。

异常传播机制

1. launch (立即抛出)

launch 启动的协程,如果遇到未捕获异常,会立即视为“处理失败”,并向上级(父协程)传播。

kotlin
// ❌ 错误:外部 try-catch 无效!
try {
    scope.launch { throw Error() }
} catch(e: Exception) { /* 捕获不到!应用崩溃 */ }

// ✅ 正确:在协程内部捕获
scope.launch {
    try { throw Error() } catch(e: Exception) { /* 捕获 */ }
}

2. async (延迟抛出)

async 启动的协程,异常会被封装在 returned Deferred 对象中。直到你调用 .await() 时才抛出。

kotlin
val deferred = scope.async {
    throw Error("Boom") // 暂时没事
}

// ... 此时还没有崩溃

try {
    deferred.await() // 💥 在这里崩溃
} catch(e: Exception) {
    // 捕获成功
}

async 的父级陷阱

如果 async 是在 coroutineScope 或普通的 Job 下启动的,即便你没有调用 await,异常也会导致父协程取消(这是结构化并发的约束)。 只有在 supervisorScope 中,async 的异常才会被完全隔离。

全局兜底:CoroutineExceptionHandler

用于捕获 所有未处理的异常

  • 注意: 此时协程已经结束了,你无法恢复它,只能做记录日志或弹窗提示。
kotlin
val handler = CoroutineExceptionHandler { ctx, e ->
    Log.e("Coroutine", "Caught $e")
}

// ✅ 有效:安装在 Scope 的根协程
scope.launch(handler) {
    throw RuntimeException() 
}

// ❌ 无效:安装在子协程
scope.launch {
    launch(handler) { // 这里 handler 不会生效!异常会直接传播给父级
        throw RuntimeException()
    }
}

生效条件

CoroutineExceptionHandler 仅在 根协程 (Root Coroutine) 或 supervisorScope 的直接子协程中生效。

监督机制 (Supervisor)

  • SupervisorJob: 让子协程的失败变成“孤立事件”,不扩散。
  • supervisorScope: 创建一个监督作用域。
kotlin
// 场景:启动两个并行任务,A 挂了不影响 B
supervisorScope {
    val a = launch { throw Error() } // A 失败
    val b = launch { delay(100); println("B alive") } // B 继续运行
}