Skip to content

Improve docs for CoroutineExceptionHandler #1886

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

Merged
merged 5 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions docs/exception-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ Caught ArithmeticException

### CoroutineExceptionHandler

But what if one does not want to print all exceptions to the console?
[CoroutineExceptionHandler] context element is used as generic `catch` block of coroutine where custom logging or exception handling may take place.
It is similar to using [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
What if one does not want to print all exceptions to the console?
[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as generic `catch` block for
this root coroutine and all its children where custom exception handling may take place.
It is similar to [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
with the corresponding exception when the handler is called. Normally, the handler is used to
log the exception, show some kind of error message, terminate, and/or restart the application.

On JVM it is possible to redefine global exception handler for all coroutines by registering [CoroutineExceptionHandler] via
[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
Expand All @@ -89,8 +93,12 @@ Global exception handler is similar to
which is used when no more specific handlers are registered.
On Android, `uncaughtExceptionPreHandler` is installed as a global coroutine exception handler.

[CoroutineExceptionHandler] is invoked only on exceptions which are not expected to be handled by the user,
so registering it in [async] builder and the like of it has no effect.
`CoroutineExceptionHandler` is invoked only on **uncaught** exceptions — exceptions that were not handled in any other way.
In particular, all _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of
their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root,
so the `CoroutineExceptionHandler` installed in their context is never used.
In addition to that, [async] builder always catches all exceptions and represents them in the resulting [Deferred] object,
so its `CoroutineExceptionHandler` has no effect either.

<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">

Expand All @@ -102,10 +110,10 @@ fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
Expand Down Expand Up @@ -501,6 +509,8 @@ Scope is completed
[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
[SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
Expand Down
32 changes: 29 additions & 3 deletions kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,37 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
}

/**
* An optional element in the coroutine context to handle uncaught exceptions.
* An optional element in the coroutine context to handle **uncaught** exceptions.
*
* Normally, uncaught exceptions can only result from coroutines created using the [launch][CoroutineScope.launch] builder.
* Normally, uncaught exceptions can only result from _root_ coroutines created using the [launch][CoroutineScope.launch] builder.
* All _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of their
* exceptions to their parent coroutine, which also delegates to the parent, and so on until the root,
* so the `CoroutineExceptionHandler` installed in their context is never used.
* A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them
* in the resulting [Deferred] object.
* in the resulting [Deferred] object, so it cannot result in uncaught exceptions either.
*
* ### Handling coroutine exceptions
*
* `CoroutineExceptionHandler` is a last-resort mechanism for global "catch all" behavior.
* You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
* with the corresponding exception when the handler is called. Normally, the handler is used to
* log the exception, show some kind of error message, terminate, and/or restart the application.
*
* If you need to handle exception in a specific part of the code, it is recommended to use `try`/`catch` around
* the corresponding code inside your coroutine. This way you can you prevent completion of the coroutine
* with the exception (exception is now _caught_), retry the operation, and/or take other arbitrary actions:
*
* ```
* scope.launch { // launch child coroutine in a scope
* try {
* // do something
* } catch (e: Throwable) {
* // handle exception
* }
* }
* ```
*
* ### Implementation details
*
* By default, when no handler is installed, uncaught exception are handled in the following way:
* * If exception is [CancellationException] then it is ignored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
Expand Down