[v1.3.2] Can anybody explain why `CoroutineExcepti...
# coroutines
v
[v1.3.2] Can anybody explain why
CoroutineExceptionHandler
installed in top-most
runBlocking
is not being called? I read all docs but can’t quite grasp the sense of it. Example:
Copy code
fun main(): Unit = runBlocking(CoroutineExceptionHandler { _, e ->
    println("Boom: $e")
}) {
    (0..10).map {
        launch {
            println("Throw in attempt #$it")
            throw RuntimeException("Attempt #$it")
        }
    }
    Unit
}
s
Yup, looks like your CEH should be called. Just curious, did you try providing your CEH to the ,
launc(ceh) {...}
instead?
v
Yes, still not working
Wrapping
launch
with
supervisorScope
helps though
s
Yeah, it works, but you created a brand new top-most scope..... Need to Google this... I can imagine a CEH is not necessary, because you can put a try-catch around the runBlocking and runBlocking only returns when all its children have finished (launch in this case)
v
Yeah, I can workaround that but now I’m studying how CEHs work and can’t quite understand the logic behind
s
I wrote a blog post about it, but only with regards to ‘launch’, ‘async’ and regular
suspend
functions and such: https://medium.com/the-kotlin-chronicle/coroutine-exceptions-3378f51a7d33
👍 1
v
I read your blogpost by the way 🙂 It was very enlightening at the point (along with blogspot https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055)
s
Thanks! I should add a blurb about runBlocking….
v
Btw the whole story about top-most CEH is not working in action unfortunately. Check this example:
Copy code
fun main(): Unit = runBlocking {
    val errorHandler = CoroutineExceptionHandler { _, e ->
        println("Boom: $e")
    }
    val scope = CoroutineScope(errorHandler)
    scope.launch {
        throw RuntimeException()
    }
    Unit
}
It does not crash and it does not print anything at all. The code is similar to one you have in the blogspot
s
This works:
Copy code
fun main() {
    val ceh = CoroutineExceptionHandler { _, e ->
        println("CEH Handled Crash [$e]")  
    }

    CoroutineScope(context + ceh).launch {
        oops()
    }

    Thread.sleep(1000)
}
context
is just a dispatcher…
🤔 1
It seems
runBlocking
provides the top-most scope and it handles the exception. I think it just throws it to the caller of `runBlocking`…., even though you create a brand new top-scope right in your sample…. needs more investigation 🙂
v
Yeah maybe eventloop bootstrapped in
runBlocking
catches the exception
s
Could you try this; change
val scope = CoroutineScope(errorHandler)
into
val scope = CoroutineScope(otherDispatcher + errorHandler)
where
otherDispatcher
is a different dispatcher than the one the runBlocking is using…. It shouldn’t matter, but i’m curious….
v
Tried it, same behavior unfortunately
s
Well, at least that is to be expected, even though we may not like the answer 🙂 .
v
Debugger isn’t helping either. Next step after
throw
it disconnects 😞
s
https://stackoverflow.com/questions/53576189/coroutineexceptionhandler-not-executed-when-provided-as-launch-context > 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
v
Ah I found my mistake. A subtle one. This prints:
Copy code
fun main(): Unit = runBlocking {
    val errorHandler = CoroutineExceptionHandler { _, e ->
        println("Boom: $e")
    }
    val scope = CoroutineScope(errorHandler)
    scope.launch {
        throw RuntimeException()
    }.join()
    Unit
}
I added
join
And that was the reason why the debugger was disconnecting. With
join
I’m able to go further now
👍 1
a
The code that you wrote in the very first message is perfectly fine. The documentation hints on why that is, but one needs to be very careful to find the hint. In my opinion the docs should be clearer. So, the docs say that this handler is only ever called for uncaught exceptions. Those are which would be swallowed without a trace or passed to Thread.uncaughtExceptionHandler if it is installed. The main (the only?) source of such uncaught exceptions is a
launch
running under a
SupervisorJob
. If you call
async
under supervisor, the exections are caught be the
Deferred
job, and will be rethrown when
await
is invoked. Therefore they are not “uncaught”. Calling both
launch
or
async
under a regular
Job
results in rethrown/propagated exceptions as well. Therefore they are not “uncaught” either. So, in your original example you either need to
launch
under a
supervisorScope
, or you need to wrap your
runBlocking
in try-catch. https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/
v
This is perfect explanation, thank you! This perspective makes CEH a very weird tool. On one hand it seems like a tool designed to catch exception that you lost (as a programmer, should not happen), on the other you still can loose the exception, for example, if you forget to call
await
s
Calling
await()
allows you to try-catch an exception. Still, if the top-most scope called
launch
, its CEH will still be handling that exception. From my blog-post: > We need to be aware that even if we handle the exception by calling 
await()
, it will still go up the child-parent Coroutine chain.
v
But what if topmost coroutine is
async
? If we don’t call
await
the CEH on top will not get the exception. This is consistent with explanation above: the
async
handles the exception so it is not given to CEH. But if a user ceases to call
await
(let’s imagine he forgets) then the exception is lost. Example:
Copy code
fun main() = runBlocking {
    val scope = CoroutineScope(CoroutineExceptionHandler { _, e ->
        println("Boom: $e")
    })
    scope.async {
        throw RuntimeException()
    }.join()
}
a
In order to partially mitigate the issue with "forgetting to call await after async" Intellij (Kotlin?) provides a warning.