Корутина обрабатывает исключение в неправильном 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{} и не является корневой корутиной.

Найти описание этого поведения в документации не удалось.

Кто нибудь может объяснить что за магия здесь творится?


Ответы (0 шт):