Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CoroutineExceptionHandler not executed when provided as launch context

When I run this:

fun f() = runBlocking {
    val eh = CoroutineExceptionHandler { _, e -> trace("exception handler: $e") }
    val j1 = launch(eh) {
        trace("launched")
        delay(1000)
        throw RuntimeException("error!")
    }
    trace("joining")
    j1.join()
    trace("after join")
}
f()

This is output:

[main @coroutine#1]: joining
[main @coroutine#2]: launched
java.lang.RuntimeException: error!
    at ExceptionHandling$f9$1$j1$1.invokeSuspend(ExceptionHandling.kts:164)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
    at kotlinx.coroutines.ResumeModeKt.resumeMode(ResumeMode.kt:67)

According to the documentation for CoroutineExceptionHandler, the eh handler I provided should be executed. But it's not. Why is that?

like image 201
Julian A. Avatar asked Dec 02 '18 00:12

Julian A.


People also ask

How do you use CoroutineExceptionHandler?

CoroutineExceptionHandler context element on a root coroutine can be used as a generic catch block for this root coroutine and all its children where custom exception handling may take place. It is similar to Thread. uncaughtExceptionHandler . You cannot recover from the exception in the CoroutineExceptionHandler .

What is the difference between launch and async in kotlin coroutines?

The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred — a light-weight non-blocking future that represents a promise to provide a result later.

What is supervisorScope?

supervisorScope. The behavior of supervisorScope is defined as: a failure of a child does not cause the scope to fail and does not affect its other children. suspend fun <R> supervisorScope( block: suspend CoroutineScope.() -> R. ): R.

How do I use async with Kotlin?

Kotlin's approach to working with asynchronous code is using coroutines, which is the idea of suspendable computations, i.e. the idea that a function can suspend its execution at some point and resume later on.


2 Answers

I believe the answer lies in this section from the official coroutines docs:

If a coroutine encounters exception other than CancellationException, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency which do not depend on CoroutineExceptionHandler implementation. The original exception is handled by the parent when all its children terminate.

This also a reason why, in these examples, CoroutineExceptionHandler is always installed to a coroutine that is created in GlobalScope. It does not make sense to install an exception handler to a coroutine that is launched in the scope of the main runBlocking, since the main coroutine is going to be always cancelled when its child completes with exception despite the installed handler.

(emphasis mine)

What's described here applies not just to runBlocking and GlobalScope, but any non-top-level coroutine builder and custom scope.

To illustrate (using kotlinx.coroutines v1.0.0):

fun f() = runBlocking {
    val h1 = CoroutineExceptionHandler { _, e ->
        trace("handler 1 e: $e")
    }
    val h2 = CoroutineExceptionHandler { _, e ->
        trace("handler 2 e: $e")
    }
    val cs = CoroutineScope(newSingleThreadContext("t1"))
    trace("launching j1")
    val j1 = cs.launch(h1) {
        delay(1000)
        trace("launching j2")
        val j2 = launch(h2) {
            delay(500)
            trace("throwing exception")
            throw RuntimeException("error!")
        }
        j2.join()
    }
    trace("joining j1")
    j1.join()
    trace("exiting f")
}
f()

Output:

[main @coroutine#1]: launching j1
[main @coroutine#1]: joining j1
[t1 @coroutine#2]: launching j2
[t1 @coroutine#3]: throwing exception
[t1 @coroutine#2]: handler 1 e: java.lang.RuntimeException: error!
[main @coroutine#1]: exiting f

Note that handler h1 is executed, but h2 isn't. This is analogous to the handler on GlobalScope#launch executing, but not the handler provided to any launch inside runBlocking.

TLDR

Handlers provided to non-root coroutines of a scope will be ignored. A handler provided to the root coroutine will be executed.

As correctly pointed out by Marko Topolnik in the comments below, the above generalization only applies to coroutines created by launch. Those created by async or produce will always ignore all handlers.

like image 159
Julian A. Avatar answered Sep 28 '22 07:09

Julian A.


What is your kotlinx.coroutines version? Since 0.26.0 standalone launch builder is now deprecated and You should be using GlobalScope.launch instead.

I tried your sample and after that change it worked.

Kotlinx.coroutines changelog

like image 31
Pawel Avatar answered Sep 28 '22 06:09

Pawel