Skip to content

Commit ec111bb

Browse files
committed
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 #871
1 parent 9f6d4ff commit ec111bb

File tree

8 files changed

+69
-67
lines changed

8 files changed

+69
-67
lines changed

docs/exception-handling.md

Lines changed: 51 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,36 @@
1818

1919
## Exception Handling
2020

21-
2221
This section covers exception handling and cancellation on exceptions.
23-
We already know that cancelled coroutine throws [CancellationException] in suspension points and that it
24-
is ignored by coroutines machinery. But what happens if an exception is thrown during cancellation or multiple children of the same
25-
coroutine throw an exception?
22+
We already know that cancelled coroutine throws [CancellationException] in suspension points and that it
23+
is ignored by the coroutines' machinery. Here we look at what happens if an exception is thrown during cancellation or multiple children of the same
24+
coroutine throw an exception.
2625

2726
### Exception propagation
2827

29-
Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or
28+
Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or
3029
exposing them to users ([async] and [produce]).
31-
The former treat exceptions as unhandled, similar to Java's `Thread.uncaughtExceptionHandler`,
32-
while the latter are relying on the user to consume the final
30+
When these builders are used to create a _root_ coroutine, that is not a _child_ of another coroutine,
31+
the former builder treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`,
32+
while the latter are relying on the user to consume the final
3333
exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive]
3434
([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section).
3535

36-
It can be demonstrated by a simple example that creates coroutines in the [GlobalScope]:
36+
It can be demonstrated by a simple example that creates root coroutines using the [GlobalScope]:
3737

3838
<div class="sample" markdown="1" theme="idea" data-highlight-only>
3939

4040
```kotlin
4141
import kotlinx.coroutines.*
4242

4343
fun main() = runBlocking {
44-
val job = GlobalScope.launch {
44+
val job = GlobalScope.launch { // root coroutine with launch
4545
println("Throwing exception from launch")
4646
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
4747
}
4848
job.join()
4949
println("Joined failed job")
50-
val deferred = GlobalScope.async {
50+
val deferred = GlobalScope.async { // root coroutine with async
5151
println("Throwing exception from async")
5252
throw ArithmeticException() // Nothing is printed, relying on user to call await
5353
}
@@ -78,7 +78,7 @@ Caught ArithmeticException
7878

7979
### CoroutineExceptionHandler
8080

81-
What if one does not want to print all exceptions to the console?
81+
It is possible to customize the default behavior of printing **uncaught** exceptions to the console.
8282
[CoroutineExceptionHandler] context element on a _root_ coroutine can be used as generic `catch` block for
8383
this root coroutine and all its children where custom exception handling may take place.
8484
It is similar to [`Thread.uncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setUncaughtExceptionHandler(java.lang.Thread.UncaughtExceptionHandler)).
@@ -100,6 +100,9 @@ so the `CoroutineExceptionHandler` installed in their context is never used.
100100
In addition to that, [async] builder always catches all exceptions and represents them in the resulting [Deferred] object,
101101
so its `CoroutineExceptionHandler` has no effect either.
102102

103+
> Coroutines running in supervision scope do not propagate exceptions to their parent and are
104+
excluded from this rule. A further [Supervision](#supervision) section of this document gives more details.
105+
103106
<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
104107

105108
```kotlin
@@ -108,7 +111,7 @@ import kotlinx.coroutines.*
108111
fun main() = runBlocking {
109112
//sampleStart
110113
val handler = CoroutineExceptionHandler { _, exception ->
111-
println("Caught $exception")
114+
println("CoroutineExceptionHandler got $exception")
112115
}
113116
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
114117
throw AssertionError()
@@ -128,14 +131,14 @@ fun main() = runBlocking {
128131
The output of this code is:
129132

130133
```text
131-
Caught java.lang.AssertionError
134+
CoroutineExceptionHandler got java.lang.AssertionError
132135
```
133136

134137
<!--- TEST-->
135138

136139
### Cancellation and exceptions
137140

138-
Cancellation is tightly bound with exceptions. Coroutines internally use `CancellationException` for cancellation, these
141+
Cancellation is closely related to exceptions. Coroutines internally use `CancellationException` for cancellation, these
139142
exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can
140143
be obtained by `catch` block.
141144
When a coroutine is cancelled using [Job.cancel], it terminates, but it does not cancel its parent.
@@ -183,15 +186,17 @@ Parent is not cancelled
183186

184187
If a coroutine encounters an exception other than `CancellationException`, it cancels its parent with that exception.
185188
This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for
186-
[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async) which do not depend on
187-
[CoroutineExceptionHandler] implementation.
188-
The original exception is handled by the parent when all its children terminate.
189+
[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async).
190+
[CoroutineExceptionHandler] implementation is not used for child coroutines.
189191

190-
> This also a reason why, in these examples, [CoroutineExceptionHandler] is always installed to a coroutine
192+
> In these examples [CoroutineExceptionHandler] is always installed to a coroutine
191193
that is created in [GlobalScope]. It does not make sense to install an exception handler to a coroutine that
192194
is launched in the scope of the main [runBlocking], since the main coroutine is going to be always cancelled
193195
when its child completes with exception despite the installed handler.
194196

197+
The original exception is handled by the parent only when all its children terminate,
198+
which is demonstrated by the following example.
199+
195200
<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
196201

197202
```kotlin
@@ -200,7 +205,7 @@ import kotlinx.coroutines.*
200205
fun main() = runBlocking {
201206
//sampleStart
202207
val handler = CoroutineExceptionHandler { _, exception ->
203-
println("Caught $exception")
208+
println("CoroutineExceptionHandler got $exception")
204209
}
205210
val job = GlobalScope.launch(handler) {
206211
launch { // the first child
@@ -235,22 +240,15 @@ The output of this code is:
235240
Second child throws an exception
236241
Children are cancelled, but exception is not handled until all children terminate
237242
The first child finished its non cancellable block
238-
Caught java.lang.ArithmeticException
243+
CoroutineExceptionHandler got java.lang.ArithmeticException
239244
```
240245
<!--- TEST-->
241246

242247
### Exceptions aggregation
243248

244-
What happens if multiple children of a coroutine throw an exception?
245-
The general rule is "the first exception wins", so the first thrown exception is exposed to the handler.
246-
But that may cause lost exceptions, for example if coroutine throws an exception in its `finally` block.
247-
So, additional exceptions are suppressed.
248-
249-
> One of the solutions would have been to report each exception separately,
250-
but then [Deferred.await] should have had the same mechanism to avoid behavioural inconsistency and this
251-
would cause implementation details of a coroutines (whether it had delegated parts of its work to its children or not)
252-
to leak to its exception handler.
253-
249+
When multiple children of a coroutine fail with an exception the
250+
general rule is "the first exception wins", so the first exception gets handed.
251+
All additional exceptions that happen after the first one are attached to the first exception as suppressed ones.
254252

255253
<!--- INCLUDE
256254
import kotlinx.coroutines.exceptions.*
@@ -264,19 +262,19 @@ import java.io.*
264262

265263
fun main() = runBlocking {
266264
val handler = CoroutineExceptionHandler { _, exception ->
267-
println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
265+
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
268266
}
269267
val job = GlobalScope.launch(handler) {
270268
launch {
271269
try {
272-
delay(Long.MAX_VALUE)
270+
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
273271
} finally {
274-
throw ArithmeticException()
272+
throw ArithmeticException() // the second exception
275273
}
276274
}
277275
launch {
278276
delay(100)
279-
throw IOException()
277+
throw IOException() // the first exception
280278
}
281279
delay(Long.MAX_VALUE)
282280
}
@@ -293,15 +291,15 @@ fun main() = runBlocking {
293291
The output of this code is:
294292

295293
```text
296-
Caught java.io.IOException with suppressed [java.lang.ArithmeticException]
294+
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
297295
```
298296

299297
<!--- TEST-->
300298

301299
> Note, this mechanism currently works only on Java version 1.7+.
302300
Limitation on JS and Native is temporary and will be fixed in the future.
303301

304-
Cancellation exceptions are transparent and unwrapped by default:
302+
Cancellation exceptions are transparent and are unwrapped by default:
305303

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

@@ -312,21 +310,21 @@ import java.io.*
312310
fun main() = runBlocking {
313311
//sampleStart
314312
val handler = CoroutineExceptionHandler { _, exception ->
315-
println("Caught original $exception")
313+
println("CoroutineExceptionHandler got $exception")
316314
}
317315
val job = GlobalScope.launch(handler) {
318-
val inner = launch {
316+
val inner = launch { // all this stack of coroutines will get cancelled
319317
launch {
320318
launch {
321-
throw IOException()
319+
throw IOException() // the original exception
322320
}
323321
}
324322
}
325323
try {
326324
inner.join()
327325
} catch (e: CancellationException) {
328326
println("Rethrowing CancellationException with original cause")
329-
throw e
327+
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
330328
}
331329
}
332330
job.join()
@@ -342,25 +340,26 @@ The output of this code is:
342340

343341
```text
344342
Rethrowing CancellationException with original cause
345-
Caught original java.io.IOException
343+
CoroutineExceptionHandler got java.io.IOException
346344
```
347345
<!--- TEST-->
348346

349347
### Supervision
350348

351349
As we have studied before, cancellation is a bidirectional relationship propagating through the whole
352-
coroutines hierarchy. But what if unidirectional cancellation is required?
350+
hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required.
353351

354352
A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI's child tasks
355353
have failed, it is not always necessary to cancel (effectively kill) the whole UI component,
356-
but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer required.
354+
but if UI component is destroyed (and its job is cancelled), then it is necessary to fail all child jobs as their results are no longer needed.
357355

358356
Another example is a server process that spawns several children jobs and needs to _supervise_
359357
their execution, tracking their failures and restarting just those children jobs that had failed.
360358

361359
#### Supervision job
362360

363-
For these purposes [SupervisorJob][SupervisorJob()] can be used. It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated
361+
For these purposes [SupervisorJob][SupervisorJob()] can be used.
362+
It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated
364363
only downwards. It is easy to demonstrate with an example:
365364

366365
<div class="sample" markdown="1" theme="idea" data-highlight-only>
@@ -414,8 +413,8 @@ Second child is cancelled because supervisor is cancelled
414413

415414
#### Supervision scope
416415

417-
For *scoped* concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation
418-
only in one direction and cancels all children only if it has failed itself. It also waits for all children before completion
416+
For _scoped_ concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation
417+
in one direction only and cancels all children only if it has failed itself. It also waits for all children before completion
419418
just like [coroutineScope] does.
420419

421420
<div class="sample" markdown="1" theme="idea" data-highlight-only>
@@ -463,8 +462,11 @@ Caught assertion error
463462
#### Exceptions in supervised coroutines
464463

465464
Another crucial difference between regular and supervisor jobs is exception handling.
466-
Every child should handle its exceptions by itself via exception handling mechanisms.
465+
Every child should handle its exceptions by itself via exception handling mechanism.
467466
This difference comes from the fact that child's failure is not propagated to the parent.
467+
It means that coroutines launched directly inside [supervisorScope] _do_ use the [CoroutineExceptionHandler]
468+
that is installed in their scope in the same way as root coroutines do
469+
(see [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details).
468470

469471
<div class="sample" markdown="1" theme="idea" data-highlight-only>
470472

@@ -474,7 +476,7 @@ import kotlinx.coroutines.*
474476

475477
fun main() = runBlocking {
476478
val handler = CoroutineExceptionHandler { _, exception ->
477-
println("Caught $exception")
479+
println("CoroutineExceptionHandler got $exception")
478480
}
479481
supervisorScope {
480482
val child = launch(handler) {
@@ -496,7 +498,7 @@ The output of this code is:
496498
```text
497499
Scope is completing
498500
Child throws an exception
499-
Caught java.lang.AssertionError
501+
CoroutineExceptionHandler got java.lang.AssertionError
500502
Scope is completed
501503
```
502504
<!--- TEST-->

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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 1 deletion
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) { // root coroutine, running in GlobalScope
1515
throw AssertionError()

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 1 addition & 1 deletion
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) {

0 commit comments

Comments
 (0)