Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mtopolnik opened this issue Jan 3, 2020 · 5 comments

Comments

@mtopolnik
Copy link
Contributor

mtopolnik commented Jan 3, 2020

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.

@mtopolnik mtopolnik changed the title CoroutineExceptionHandler not handling exceptions CoroutineExceptionHandler: Issues with Documentation, Possible Bug in runBlocking Jan 5, 2020
@elizarov
Copy link
Contributor

The docs are indeed vague there, but it seems to me that runBlocking's behaves as intended. The key things to understand is that structured concurrency takes precedense. The exception always travels up the coroutine hierarchy to its root. Only at the root it can become an "unhandled exception" in which case the root's CoroutineExceptionHandler is used.

In this particular example there's no unhandled exception at all. runBlocking { ... } handles all exceptions in its children by rethrowing them to the caller, so CoroutineExceptionHandler will not be used.

@mtopolnik
Copy link
Contributor Author

There is often confusion as to how exactly the exception handler works. There is a steady trickle of questions on Stack Overflow and I basically had to infer the rule myself, it wasn't clearly spelled out. I intended this primarily as a documentation issue.

@elizarov
Copy link
Contributor

@mtopolnik Here's my attempt and improving the corresponding docs to reduce confusion: #1886

qwwdfsad pushed a commit that referenced this issue Apr 24, 2020
* Further clarifications and better style for exception handling
* Consistent terminology on "uncaught exceptions".
* Clarified special relations of exception handling with supervision.
* Clearer text in CoroutineExceptionHandler examples.
* Minor stylistic corrections.

Fixes #1746
Fixes #871

Co-Authored-By: EdwarDDay <[email protected]>
recheej pushed a commit to recheej/kotlinx.coroutines that referenced this issue Dec 28, 2020
* Further clarifications and better style for exception handling
* Consistent terminology on "uncaught exceptions".
* Clarified special relations of exception handling with supervision.
* Clearer text in CoroutineExceptionHandler examples.
* Minor stylistic corrections.

Fixes Kotlin#1746
Fixes Kotlin#871

Co-Authored-By: EdwarDDay <[email protected]>
@marcskow
Copy link

Hi, it's still unclear to me.

The exception always travels up the coroutine hierarchy to its root. Only at the root it can become an "unhandled exception" in which case the root's CoroutineExceptionHandler is used.

Does it mean that runBlocking intentionally handles exceptions by throwing them immediately i.e. it fits into the whole mechanism but as root it already has a defined behavior which is re-throwing and that's why CoroutineExceptionHandler is not used (because it is used only when there is not a defined exception-handling behaviour).

Or it means that CoroutineExceptionHandler context is ignored by runBlocking? If runBlocking is a root passing CoroutineExceptionHandler to it as an argument should be taken into consideration shouldn't it?

@dkhalanskyjb
Copy link
Collaborator

Does it mean that runBlocking intentionally handles exceptions by throwing them immediately

Yes, this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants