Skip to content

Commit 33f923d

Browse files
elizarovEdwarDDay
authored andcommitted
Improve docs for CoroutineExceptionHandler (Kotlin#1886)
* 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]>
1 parent 7103e65 commit 33f923d

9 files changed

+118
-82
lines changed

docs/exception-handling.md

+67-55
Large diffs are not rendered by default.

kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt

+31-7
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,38 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
5252
}
5353

5454
/**
55-
* An optional element in the coroutine context to handle uncaught exceptions.
55+
* An optional element in the coroutine context to handle **uncaught** exceptions.
5656
*
57-
* Normally, uncaught exceptions can only result from coroutines created using the [launch][CoroutineScope.launch] builder.
57+
* Normally, uncaught exceptions can only result from _root_ coroutines created using the [launch][CoroutineScope.launch] builder.
58+
* All _children_ coroutines (coroutines created in the context of another [Job]) delegate handling of their
59+
* exceptions to their parent coroutine, which also delegates to the parent, and so on until the root,
60+
* so the `CoroutineExceptionHandler` installed in their context is never used.
61+
* Coroutines running with [SupervisorJob] do not propagate exceptions to their parent and are treated like root coroutines.
5862
* A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them
59-
* in the resulting [Deferred] object.
63+
* in the resulting [Deferred] object, so it cannot result in uncaught exceptions.
64+
*
65+
* ### Handling coroutine exceptions
66+
*
67+
* `CoroutineExceptionHandler` is a last-resort mechanism for global "catch all" behavior.
68+
* You cannot recover from the exception in the `CoroutineExceptionHandler`. The coroutine had already completed
69+
* with the corresponding exception when the handler is called. Normally, the handler is used to
70+
* log the exception, show some kind of error message, terminate, and/or restart the application.
71+
*
72+
* If you need to handle exception in a specific part of the code, it is recommended to use `try`/`catch` around
73+
* the corresponding code inside your coroutine. This way you can prevent completion of the coroutine
74+
* with the exception (exception is now _caught_), retry the operation, and/or take other arbitrary actions:
75+
*
76+
* ```
77+
* scope.launch { // launch child coroutine in a scope
78+
* try {
79+
* // do something
80+
* } catch (e: Throwable) {
81+
* // handle exception
82+
* }
83+
* }
84+
* ```
85+
*
86+
* ### Implementation details
6087
*
6188
* By default, when no handler is installed, uncaught exception are handled in the following way:
6289
* * If exception is [CancellationException] then it is ignored
@@ -66,10 +93,7 @@ public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineConte
6693
* * Otherwise, all instances of [CoroutineExceptionHandler] found via [ServiceLoader]
6794
* * and current thread's [Thread.uncaughtExceptionHandler] are invoked.
6895
*
69-
* [CoroutineExceptionHandler] can be invoked from an arbitrary dispatcher used by coroutines in the current job hierarchy.
70-
* For example, if one has a `MainScope` and launches children of the scope in main and default dispatchers, then exception handler can
71-
* be invoked either in main or in default dispatcher thread regardless of
72-
* which particular dispatcher coroutine that has thrown an exception used.
96+
* [CoroutineExceptionHandler] can be invoked from an arbitrary thread.
7397
*/
7498
public interface CoroutineExceptionHandler : CoroutineContext.Element {
7599
/**

kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ package kotlinx.coroutines.guide.exampleExceptions01
88
import kotlinx.coroutines.*
99

1010
fun main() = runBlocking {
11-
val job = GlobalScope.launch {
11+
val job = GlobalScope.launch { // root coroutine with launch
1212
println("Throwing exception from launch")
1313
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
1414
}
1515
job.join()
1616
println("Joined failed job")
17-
val deferred = GlobalScope.async {
17+
val deferred = GlobalScope.async { // root coroutine with async
1818
println("Throwing exception from async")
1919
throw ArithmeticException() // Nothing is printed, relying on user to call await
2020
}

kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import kotlinx.coroutines.*
99

1010
fun main() = runBlocking {
1111
val handler = CoroutineExceptionHandler { _, exception ->
12-
println("Caught $exception")
12+
println("CoroutineExceptionHandler got $exception")
1313
}
14-
val job = GlobalScope.launch(handler) {
14+
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
1515
throw AssertionError()
1616
}
17-
val deferred = GlobalScope.async(handler) {
17+
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
1818
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
1919
}
2020
joinAll(job, deferred)

kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.coroutines.*
99

1010
fun main() = runBlocking {
1111
val handler = CoroutineExceptionHandler { _, exception ->
12-
println("Caught $exception")
12+
println("CoroutineExceptionHandler got $exception")
1313
}
1414
val job = GlobalScope.launch(handler) {
1515
launch { // the first child

kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ import java.io.*
1212

1313
fun main() = runBlocking {
1414
val handler = CoroutineExceptionHandler { _, exception ->
15-
println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
15+
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
1616
}
1717
val job = GlobalScope.launch(handler) {
1818
launch {
1919
try {
20-
delay(Long.MAX_VALUE)
20+
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
2121
} finally {
22-
throw ArithmeticException()
22+
throw ArithmeticException() // the second exception
2323
}
2424
}
2525
launch {
2626
delay(100)
27-
throw IOException()
27+
throw IOException() // the first exception
2828
}
2929
delay(Long.MAX_VALUE)
3030
}

kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ import java.io.*
1010

1111
fun main() = runBlocking {
1212
val handler = CoroutineExceptionHandler { _, exception ->
13-
println("Caught original $exception")
13+
println("CoroutineExceptionHandler got $exception")
1414
}
1515
val job = GlobalScope.launch(handler) {
16-
val inner = launch {
16+
val inner = launch { // all this stack of coroutines will get cancelled
1717
launch {
1818
launch {
19-
throw IOException()
19+
throw IOException() // the original exception
2020
}
2121
}
2222
}
2323
try {
2424
inner.join()
2525
} catch (e: CancellationException) {
2626
println("Rethrowing CancellationException with original cause")
27-
throw e
27+
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
2828
}
2929
}
3030
job.join()

kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import kotlinx.coroutines.*
1010

1111
fun main() = runBlocking {
1212
val handler = CoroutineExceptionHandler { _, exception ->
13-
println("Caught $exception")
13+
println("CoroutineExceptionHandler got $exception")
1414
}
1515
supervisorScope {
1616
val child = launch(handler) {

kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ExceptionsGuideTest {
2222
@Test
2323
fun testExampleExceptions02() {
2424
test("ExampleExceptions02") { kotlinx.coroutines.guide.exampleExceptions02.main() }.verifyLines(
25-
"Caught java.lang.AssertionError"
25+
"CoroutineExceptionHandler got java.lang.AssertionError"
2626
)
2727
}
2828

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

4848
@Test
4949
fun testExampleExceptions05() {
5050
test("ExampleExceptions05") { kotlinx.coroutines.guide.exampleExceptions05.main() }.verifyLines(
51-
"Caught java.io.IOException with suppressed [java.lang.ArithmeticException]"
51+
"CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]"
5252
)
5353
}
5454

5555
@Test
5656
fun testExampleExceptions06() {
5757
test("ExampleExceptions06") { kotlinx.coroutines.guide.exampleExceptions06.main() }.verifyLines(
5858
"Rethrowing CancellationException with original cause",
59-
"Caught original java.io.IOException"
59+
"CoroutineExceptionHandler got java.io.IOException"
6060
)
6161
}
6262

@@ -85,7 +85,7 @@ class ExceptionsGuideTest {
8585
test("ExampleSupervision03") { kotlinx.coroutines.guide.exampleSupervision03.main() }.verifyLines(
8686
"Scope is completing",
8787
"Child throws an exception",
88-
"Caught java.lang.AssertionError",
88+
"CoroutineExceptionHandler got java.lang.AssertionError",
8989
"Scope is completed"
9090
)
9191
}

0 commit comments

Comments
 (0)