Skip to content

CoroutineExceptionHandler: Issues with Documentation, Possible Bug in runBlocking #1746

Closed
@mtopolnik

Description

@mtopolnik

Given this code:

val job = Job()

val scope = CoroutineScope(job)

val handler = CoroutineExceptionHandler { coroCtx, e ->
    println("Handler caught $e")
}

fun CoroutineScope.handlerTest() = this.launch(handler) {
    throw Exception("Launched coroutine failed")
}

When I run it like this (using the scope constructed above):

scope.handlerTest()

I get the expected output:

Handler caught java.lang.Exception: Launched coroutine failed

However, when I run it like this (using the runBlocking scope):

runBlocking { this.handlerTest() }

the handler doesn't observe the exception and I get the default handler's output instead:

Exception in thread "main" java.lang.Exception: Launched coroutine failed
	at org.mtopol.TestingKt$handlerTest$1.invokeSuspend(testing.kt:14)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    ...
	at org.mtopol.TestingKt.main(testing.kt:18)
	at org.mtopol.TestingKt.main(testing.kt)

Even when I add the handler to runBlocking:

runBlocking(handler) { this.handlerTest() }

it still crashes without the handler observing the exception.

The behavior is not specific to runBlocking, I can write the code as follows:

GlobalScope.launch { this.handlerTest() }

and I still get just a crash (now inside the Default thread pool).

Finally, if I write it as follows (adding the handler to the top-level launch):

GlobalScope.launch(handler) { this.handlerTest() }

this time it gets handled.

After studying the documentation on CoroutineExceptionHandler in detail, I found the above behaviors surprising. The documentation explicitly specifies only what happens when a coroutine exception handler is not installed, and I could only indirectly guess at what it was supposed to do when there is one. Yet my guesses were all wrong. Paraphrasing, the docs seem to claim the following:

  1. Never handle a CancellationException.
  2. If there is a Job in the context, invoke Job.cancel.
  3. Otherwise pass the exception to the exception handler.

The actual behavior is more like the following:

  1. Always invoke coroutineContext[Job]!!.cancel() (since there is always a Job in any coroutine's context)
  2. To decide whether to pass the exception to the handler, follow these rules:
    a. Never handle a CancellationException.
    b. If the current coroutine has a parent coroutine, don't handle the exception. it was passed to the parent via job.cancel().
    c. Otherwise pass the exception to the exception handler.

Furthermore, the behavior with runBlocking seems like a bug to fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions