kolboc
kolboc

Reputation: 825

Coroutines CancellationException expected behaviour

So based on Kotlin introduction to coroutines, in Cancellation and Timeouts -> Run non-cancellable block I found following explanation: Any attempt to use a suspending function in the finally block [...] causes CancellationException but when running:

fun runPlayground() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                Log.d("XXX", "job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            doWorld() // logs "World" after 1s delay
            delay(2000L)
            Log.d("XXX", "job: I'm running finally")
        }
    }
    delay(1300L) // delay a bit
    Log.d("XXX", "main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    Log.d("XXX", "main: Now I can quit.")
}

results in logging:

D/XXX: job: I'm sleeping 0 ...
D/XXX: job: I'm sleeping 1 ...
D/XXX: job: I'm sleeping 2 ...
D/XXX: main: I'm tired of waiting!
D/XXX: main: Now I can quit.

Finally block is not executed probably due to running suspend functions there, but I would expect getting CancellationException there as I run that code from:

try {
    runPlayground()
} catch (e: CancellationException)  {
    Log.d("XXX", e.message)
}

Is the exception handled internally in the coroutines?

Upvotes: 2

Views: 1996

Answers (1)

Marko Topolnik
Marko Topolnik

Reputation: 200138

The exception is thrown from doWorld() and from that point it escapes the coroutine block and gets silently swallowed because you haven't installed any unhandled exception handler. The dispatcher it was running on doesn't crash due to that exception.

If you study the documentation under the above link, you'll learn that the exception is silently swallowed only because it's a CancelationException. Any other exception would at least show up in the same manner that an unhandled exception appears in a Java thread, for example:

fun main() = runBlocking {
    launch(Job()) {
        throw Exception("I failed")
    }.join()
    println("runBlocking done")
}

This prints

Exception in thread "main" java.lang.Exception: I failed
    at org.mtopol.TestingKt$main$1$1.invokeSuspend(testing.kt:8)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:270)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at org.mtopol.TestingKt.main(testing.kt:6)
    at org.mtopol.TestingKt.main(testing.kt)

runBlocking done

Note especially that the main thread didn't actually die: it went on to print the line runBlocking done. Kotlin just reuses the installed currentThread().uncaughtExceptionHandler() to log coroutine failures.


When I went to test code similar to above, but with an installed CoroutineExceptionHandler, I found some problems:

  1. An exception handler in a child coroutine is ignored. This seems to be by design, but it's not documented.
  2. runBlocking seems to have a bug and, even if you install a handler to it, it won't run.

I created an issue about this.

Upvotes: 1

Related Questions