协程上下文异常重抛:LowLevelException转HighLevelException最佳实践
问题背景
在服务开发中大量使用Kotlin协程时,需要将服务内部抛出的LowLevelException转换为HighLevelException后向外传播。
非协程环境的可行实现
非协程场景下,直接通过try-catch即可完成异常转换:
class MyService { fun myBigComputation(type: MyServiceType) { try { for (i in (0..10_000)) { mySubComputation(type, i) } } catch (e: LowLevelException) { throw HighLevelException(type, e) } } private fun mySubComputation(type: MyServiceType, i: Int) { // 业务逻辑执行 // 触发异常时抛出 throw LowLevelException(type) } class LowLevelException(val type: MyServiceType): Exception() enum class MyServiceType { type1, type2, type3 } } class HighLevelException(val type: MyService.MyServiceType, e: Exception): Exception("Exception for type $type", e)
协程环境的第一次失败尝试
最初的协程实现无法完成预期的异常转换,LowLevelException会直接传播到外层supervisorScope,导致转换逻辑失效:
suspend fun main() = coroutineScope { val service = MyService() supervisorScope { for (type in MyService.MyServiceType.values()) { launch(CoroutineExceptionHandler { _, e -> if (e is HighLevelException) { println("ERROR: ${e.message}") } }) { service.myBigComputation(type) } } } } class MyService { suspend fun myBigComputation(type: MyServiceType) = coroutineScope { launch(CoroutineExceptionHandler { _, e -> if (e is LowLevelException) { throw HighLevelException(type, e) } }) { for (i in (0..10)) { mySubComputation(type, i) } } } private fun mySubComputation(type: MyServiceType, i: Int) { if (i < 5 || type != MyServiceType.type1) { print(".") } else { println("something bad happened but exception kill all") throw LowLevelException(type) } } class LowLevelException(val type: MyServiceType): Exception() enum class MyServiceType { type1, type2, type3 } } class HighLevelException(val type: MyService.MyServiceType, e: Exception): Exception("Exception for type $type", e)
问题根源:coroutineScope是失败传播型作用域,子协程抛出未捕获异常时会立即取消整个作用域,内部的CoroutineExceptionHandler还未完成转换,异常就直接向外扩散了。
改用supervisorScope的可行方案
替换为supervisorScope后功能正常,但不确定是否为最优解:
suspend fun main() = coroutineScope { val service = MyService() supervisorScope { for (type in MyService.MyServiceType.values()) { launch(CoroutineExceptionHandler { _, e -> if (e is HighLevelException) { println("ERROR: ${e.message}") } }) { service.myBigComputation(type) } } } } class MyService { suspend fun myBigComputation(type: MyServiceType) = supervisorScope { launch(CoroutineExceptionHandler { _, e -> if (e is LowLevelException) { throw HighLevelException(type, e) } }) { for (i in (0..10)) { mySubComputation(type, i) } } } // 其余代码同前 }
最佳实现方式
方式1:直接在挂起函数内用try-catch(最简洁)
如果myBigComputation不需要并行执行子任务,完全不需要额外启动协程或创建作用域,直接沿用非协程的异常处理逻辑即可——挂起函数支持常规的try-catch:
class MyService { suspend fun myBigComputation(type: MyServiceType) { try { for (i in (0..10)) { mySubComputation(type, i) } } catch (e: LowLevelException) { throw HighLevelException(type, e) } } private fun mySubComputation(type: MyServiceType, i: Int) { // 业务逻辑同前 } // 异常类、枚举同前 }
外层调用时,转换后的HighLevelException会正常传播到launch绑定的CoroutineExceptionHandler中,完全符合预期。
方式2:并行任务场景下的最优处理
如果myBigComputation需要并行执行多个子任务,推荐以下方案:
- 用
supervisorScope隔离子任务失败(避免单个子任务失败导致整个任务取消) - 子任务内部直接用
try-catch做异常转换,无需依赖CoroutineExceptionHandler
示例:
class MyService { suspend fun myBigComputation(type: MyServiceType) = supervisorScope { // 并行启动多个子任务 repeat(10) { i -> launch { try { mySubComputation(type, i) } catch (e: LowLevelException) { throw HighLevelException(type, e) } } } } }
这种方式将异常转换逻辑与业务代码紧耦合,可读性更强,比在launch上绑定CoroutineExceptionHandler更直观。
为什么不推荐用CoroutineExceptionHandler做转换?
CoroutineExceptionHandler的设计初衷是全局异常兜底处理(如日志上报),而非业务层面的异常转换。它仅能捕获未被任何try-catch处理的异常,且一旦在Handler中重新抛出异常,会直接传播到父协程/作用域,无法再被当前协程的其他逻辑捕获,灵活性远不如直接的try-catch。
内容的提问来源于stack exchange,提问作者Benjamin Jalon

