kotlinkotlin-coroutinescoroutine

Why does not exception handled by parent coroutine exception handler?


I test coroutineExceptionHandler.

Here is my code.

    fun createExceptionHandler(name: String) = CoroutineExceptionHandler { context, throwable ->
        println("[${Thread.currentThread().name} - $name] Caught $throwable\n${context[CoroutineExceptionHandler]}")
    }

    @Test
    fun coroutineScope_exceptionHandle() = runBlocking<Unit> {
        val coroutineScope = CoroutineScope(Job() + createExceptionHandler("supervisor"))
        coroutineScope.launch(createExceptionHandler("launch1")) {
            launch(CoroutineName("launch2") + createExceptionHandler("launch2")) {
                throw Exception("[${Thread.currentThread().name}] Error !")
            }
            launch(CoroutineName("launch3")) {
                println("[${Thread.currentThread().name}] launch3")
            }
        }
        delay(1000L)
    }

Result:

[DefaultDispatcher-worker-3 @launch3#6] launch3
[DefaultDispatcher-worker-2 @launch2#5 - launch1] Caught java.lang.Exception: [DefaultDispatcher-worker-2 @launch2#5] Error !
...

I expect the exception handled by coroutineScope's handler named supervisor.

Could you please explain why the exception handled by launch1 handler?


Solution

  • First - CoroutineExceptionHandler is only intended as a final consumer of uncaught exceptions. Installing it on child coroutines is not intended.

    Second - Exception propagation in coroutines comes into play here. When a coroutine fails (throws) it fails its parent as well by forwarding exception to it. In that sense exception is not considered uncaught so exception handler of child has no need to "handle" it.

    Only when there's no parent to forward exception to (root coroutine) the exception is considered uncaught and given to its exception handler.

    In your particular case launch1 is root coroutine (launched directly in a scope) and its exception handler is being used to handle uncaught exception of its child - launch2. Failure of launch1 does cause the scope to get cancelled but exception that did it was already handled so its not forwarded to "supervisor" handler.