显式地将协程上下文传递给异步调用会产生不同的异常处理行为,而不是将其安装在封闭范围内

Explicitly passing a coroutine context to an async call produces different exception handling behavior vs. installing it in the enclosing scope

以下代码同时输出 "Handled by exception handler" 和 "Caught exception" 消息:

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() {
    val eh = CoroutineExceptionHandler { _, e -> println("Handled by exception handler") }
    val context = eh + Job()

    CoroutineScope(context).launch {
        val res = async<String> { throw RuntimeException() }
//        val res = async<String>(context) { throw RuntimeException() }

        try {
            println("Result: ${res.await()}")
        }
        catch (e: Throwable){
            println("Caught exception")
        }
    }


    Thread.sleep(1000)
}

但是如果我交换注释的 "val res" 行,我只会收到 "Caught exception" 消息。为什么显式向 async 提供 CoroutineContext(其中包括异常处理程序)会导致异常处理程序不处理异常?

答案埋在文档中,here:

Normally, uncaught exceptions can only result from coroutines created using the launch builder. A coroutine that was created using async always catches all its exceptions and represents them in the resulting Deferred object.

here:

The parent job is inherited from a CoroutineScope as well, but it can also be overridden with corresponding coroutineContext element.

第一种情况:

val res = async<String> { throw RuntimeException() }

Kotlin 通过添加新的 Job 实例为新协程创建上下文,该实例是通过协程范围继承的作业的子实例。因此,当此协程失败时,它会通知其父级,然后将其带到已安装的异常处理程序。

第二种情况:

val res = async<String>(context) { throw RuntimeException() }

context 已经包含一个 Job 元素。这会覆盖上述行为,并且不会为新协程创建新作业。因此它的 Job 元素不指向作为父范围的工作。当它失败时,协程不会按照引用的文档将异常传递给处理程序,也不会将其传递给不存在的父级。

经验教训:永远不要将带有 Job 元素的上下文传递给子 async 构建器。