Корутина обрабатывает исключение в неправильном CoroutineExceptionHandler
Наткнулся на невозможное (согласно документации) поведение корутин. Что бы его воспроизвести написал следующий код:
private val scopeHandler = CoroutineExceptionHandler { _, exception ->
println("Scope handler: ${exception.message}")
}
private val rootCoroutineHandler = CoroutineExceptionHandler { _, exception ->
println("Root coroutine handler: ${exception.message}")
}
private val childCoroutineHandler = CoroutineExceptionHandler { _, exception ->
println("Child coroutine handler: ${exception.message}")
}
private val job = Job()
private val customScope = CoroutineScope(Dispatchers.IO + job + scopeHandler)
private fun CoroutineScope.name(): String {
return "coroutine-${this.coroutineContext[CoroutineName]?.name ?: "No name"}"
}
private suspend fun executeSumSuspendFunc(name: String, delay: Long, throwEx: Boolean = false) {
try {
println("$name start")
delay(delay)
if (throwEx) {
println("$name throw Exception")
throw Exception("Exception in $name")
}
println("$name end")
} catch (e: CancellationException) {
println("$name cancel")
throw e
}
}
private suspend fun getData() = coroutineScope {
launch(CoroutineName("4")) {
executeSumSuspendFunc(name(), 1000)
}
launch(CoroutineName("5")) {
executeSumSuspendFunc(name(), 500, true)
}
}
fun execute() {
customScope.launch(CoroutineName("1") + rootCoroutineHandler) {
println("${name()} start")
launch(CoroutineName("2")) {
executeSumSuspendFunc(name(), 1000)
}
coroutineScope {
launch(CoroutineName("3") + childCoroutineHandler) {
println("${name()} start")
try {
getData()
} catch (e: Exception) {
println("${name()} catch ${e.message}")
throw e
}
println("${name()} end")
}
}
println("${name()} end")
}
}
Результат:
coroutine-1 start
coroutine-2 start
coroutine-3 start
coroutine-4 start
coroutine-5 start
coroutine-5 throw Exception
coroutine-4 cancel
coroutine-3 catch Exception in coroutine-5
coroutine-2 cancel
Root coroutine handler: Exception in coroutine-5
Корутина 5 выбрасывает необрабатываемое исключение. Корутины 1 и 3 падают. Корутины 2 и 4 отменяются. Исключение попадает в обработчик корневой корутины. Это ожидаемое поведение
Теперь в функции getData() заменим coroutineScope на supervisorScope
private suspend fun getData() = supervisorScope {
И получим следующее:
coroutine-1 start
coroutine-2 start
coroutine-3 start
coroutine-4 start
coroutine-5 start
coroutine-5 throw Exception
Child coroutine handler: Exception in coroutine-5
coroutine-2 end
coroutine-4 end
coroutine-3 end
coroutine-1 end
Все корутины отработали ожидаемым образом, но исключение попало не в обработчик корневой корутины а в обработчик дочерней, который согласно документации вообще никогда не должен быть вызван т.к. корутина 3 запущена в coroutineScope{} и не является корневой корутиной.
Найти описание этого поведения в документации не удалось.
Кто нибудь может объяснить что за магия здесь творится?