Skip to content

Commit

Permalink
Improve docs for CoroutineExceptionHandler (Kotlin#1886)
Browse files Browse the repository at this point in the history
* 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 <4127904+EdwarDDay@users.noreply.github.com>
  • Loading branch information
2 people authored and recheej committed Dec 28, 2020
1 parent 7103e65 commit 33f923d
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 82 deletions.
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

0 comments on commit 33f923d

Please sign in to comment.