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 all 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
122 changes: 67 additions & 55 deletions docs/exception-handling.md

Large diffs are not rendered by default.

38 changes: 31 additions & 7 deletions kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,38 @@ 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.
* Coroutines running with [SupervisorJob] do not propagate exceptions to their parent and are treated like root coroutines.
* 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.
*
* ### 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 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 All @@ -66,10 +93,7 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
* * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader]
* * and current thread's [Thread.uncaughtExceptionHandler] are invoked.
*
* [CoroutineExceptionHandler] can be invoked from an arbitrary dispatcher used by coroutines in the current job hierarchy.
* For example, if one has a `MainScope` and launches children of the scope in main and default dispatchers, then exception handler can
* be invoked either in main or in default dispatcher thread regardless of
* which particular dispatcher coroutine that has thrown an exception used.
* [CoroutineExceptionHandler] can be invoked from an arbitrary thread.
*/
public interface CoroutineExceptionHandler : CoroutineContext.Element {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ package kotlinx.coroutines.guide.exampleExceptions01
import kotlinx.coroutines.*

fun main() = runBlocking {
val job = GlobalScope.launch {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async {
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import kotlinx.coroutines.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
println("CoroutineExceptionHandler got $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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.coroutines.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import java.io.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE)
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
} finally {
throw ArithmeticException()
throw ArithmeticException() // the second exception
}
}
launch {
delay(100)
throw IOException()
throw IOException() // the first exception
}
delay(Long.MAX_VALUE)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import java.io.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught original $exception")
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch {
val inner = launch { // all this stack of coroutines will get cancelled
launch {
launch {
throw IOException()
throw IOException() // the original exception
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.coroutines.*

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ExceptionsGuideTest {
@Test
fun testExampleExceptions02() {
test("ExampleExceptions02") { kotlinx.coroutines.guide.exampleExceptions02.main() }.verifyLines(
"Caught java.lang.AssertionError"
"CoroutineExceptionHandler got java.lang.AssertionError"
)
}

Expand All @@ -41,22 +41,22 @@ class ExceptionsGuideTest {
"Second child throws an exception",
"Children are cancelled, but exception is not handled until all children terminate",
"The first child finished its non cancellable block",
"Caught java.lang.ArithmeticException"
"CoroutineExceptionHandler got java.lang.ArithmeticException"
)
}

@Test
fun testExampleExceptions05() {
test("ExampleExceptions05") { kotlinx.coroutines.guide.exampleExceptions05.main() }.verifyLines(
"Caught java.io.IOException with suppressed [java.lang.ArithmeticException]"
"CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]"
)
}

@Test
fun testExampleExceptions06() {
test("ExampleExceptions06") { kotlinx.coroutines.guide.exampleExceptions06.main() }.verifyLines(
"Rethrowing CancellationException with original cause",
"Caught original java.io.IOException"
"CoroutineExceptionHandler got java.io.IOException"
)
}

Expand Down Expand Up @@ -85,7 +85,7 @@ class ExceptionsGuideTest {
test("ExampleSupervision03") { kotlinx.coroutines.guide.exampleSupervision03.main() }.verifyLines(
"Scope is completing",
"Child throws an exception",
"Caught java.lang.AssertionError",
"CoroutineExceptionHandler got java.lang.AssertionError",
"Scope is completed"
)
}
Expand Down