Android ViewModel中Kotlin协程的异常处理行为疑问
一、为什么supervisorScope的异常无法被外层try-catch捕获,而coroutineScope可以?
核心差异在于两者的异常传播规则完全不同:
coroutineScope是结构化并发的默认「协同取消作用域」:
当任意子协程抛出未捕获异常时,会立即取消所有兄弟协程,随后终止自身作用域,并将异常向上传播给父协程。此时外层的try-catch包裹的是coroutineScope的代码块,异常会从coroutineScope代码块抛出,因此能被捕获。supervisorScope是专门设计的「监督作用域」,核心是让子协程异常互不影响:
子协程抛出未捕获异常时,只会取消自身,不会波及兄弟协程;但supervisorScope不会将子协程的异常捕获并抛出到自身代码块中,而是直接将异常传递给子协程上下文的CoroutineExceptionHandler。如果没有自定义处理器,异常会交给全局默认异常处理器(比如Android的崩溃监控、JVM的默认异常处理),最终还会传递到父协程(比如你的viewModelScope.launch协程),导致父协程被取消,引发应用崩溃。
回到你的代码:
viewModelScope.launch { try { supervisorScope { launch { throw Exception() } } } catch (e: Exception) { println("Exception caught") } }
异常从supervisorScope内部的子协程直接跳过了supervisorScope的代码块,传递给了父协程,而外层try-catch仅包裹了supervisorScope的执行代码,自然捕获不到这个异常。换成coroutineScope时,异常会从coroutineScope代码块抛出,因此能被外层捕获。
二、为什么runBlocking下两种场景的进程退出码不同?
Case 2:直接在runBlocking中launch子协程抛异常
runBlocking { launch { throw Exception("Launch Exception") } }
runBlocking是阻塞式作用域,会等待所有子协程完成。内部launch的子协程抛出未捕获异常时,会直接传播给runBlocking作用域,runBlocking会将该异常重新抛到JVM主线程——这属于未捕获的顶层异常,会导致JVM进程以**非0退出码(1)**终止,标记进程异常崩溃。
Case 1:runBlocking中嵌套supervisorScope的子协程抛异常
runBlocking { supervisorScope { launch { throw Exception("Supervisor launch exception") } } }
supervisorScope的子协程抛出异常时,不会导致supervisorScope本身失败,异常仅会被全局默认异常处理器处理(控制台会打印栈跟踪),但supervisorScope会正常执行完成。runBlocking等待supervisorScope结束后,没有收到任何异常,因此进程正常退出,返回0退出码。
总结:
- Case2的异常直接终止了runBlocking的执行流程,导致进程异常退出
- Case1的异常仅被默认处理器处理,未打断runBlocking的正常执行,因此进程正常退出
内容的提问来源于stack exchange,提问作者Shyam Patel

