diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 6df9f82d7e..4fe0d719e8 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -18,22 +18,22 @@ ## Exception Handling - This section covers exception handling and cancellation on exceptions. -We already know that cancelled coroutine throws [CancellationException] in suspension points and that it -is ignored by coroutines machinery. But what happens if an exception is thrown during cancellation or multiple children of the same -coroutine throw an exception? +We already know that cancelled coroutine throws [CancellationException] in suspension points and that it +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 +coroutine throw an exception. ### Exception propagation -Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or +Coroutine builders come in two flavors: propagating exceptions automatically ([launch] and [actor]) or exposing them to users ([async] and [produce]). -The former treat exceptions as unhandled, similar to Java's `Thread.uncaughtExceptionHandler`, -while the latter are relying on the user to consume the final +When these builders are used to create a _root_ coroutine, that is not a _child_ of another coroutine, +the former builder treat exceptions as **uncaught** exceptions, similar to Java's `Thread.uncaughtExceptionHandler`, +while the latter are relying on the user to consume the final exception, for example via [await][Deferred.await] or [receive][ReceiveChannel.receive] ([produce] and [receive][ReceiveChannel.receive] are covered later in [Channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) section). -It can be demonstrated by a simple example that creates coroutines in the [GlobalScope]: +It can be demonstrated by a simple example that creates root coroutines using the [GlobalScope]:
@@ -41,13 +41,13 @@ It can be demonstrated by a simple example that creates coroutines in the [Globa 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 } @@ -78,7 +78,7 @@ Caught ArithmeticException ### CoroutineExceptionHandler -What if one does not want to print all exceptions to the console? +It is possible to customize the default behavior of printing **uncaught** 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)). @@ -100,6 +100,9 @@ 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. +> Coroutines running in supervision scope do not propagate exceptions to their parent and are +excluded from this rule. A further [Supervision](#supervision) section of this document gives more details. +
```kotlin @@ -108,7 +111,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope throw AssertionError() @@ -128,14 +131,14 @@ fun main() = runBlocking { The output of this code is: ```text -Caught java.lang.AssertionError +CoroutineExceptionHandler got java.lang.AssertionError ``` ### Cancellation and exceptions -Cancellation is tightly bound with exceptions. Coroutines internally use `CancellationException` for cancellation, these +Cancellation is closely related to exceptions. Coroutines internally use `CancellationException` for cancellation, these exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can be obtained by `catch` block. 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 If a coroutine encounters an exception other than `CancellationException`, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for -[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async) which do not depend on -[CoroutineExceptionHandler] implementation. -The original exception is handled by the parent when all its children terminate. +[structured concurrency](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/composing-suspending-functions.md#structured-concurrency-with-async). +[CoroutineExceptionHandler] implementation is not used for child coroutines. -> This also a reason why, in these examples, [CoroutineExceptionHandler] is always installed to a coroutine +> In these examples [CoroutineExceptionHandler] is always installed to a coroutine that is created in [GlobalScope]. It does not make sense to install an exception handler to a coroutine that is launched in the scope of the main [runBlocking], since the main coroutine is going to be always cancelled when its child completes with exception despite the installed handler. +The original exception is handled by the parent only when all its children terminate, +which is demonstrated by the following example. +
```kotlin @@ -200,7 +205,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { //sampleStart val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { launch { // the first child @@ -235,22 +240,15 @@ The output of this code is: 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 ``` ### Exceptions aggregation -What happens if multiple children of a coroutine throw an exception? -The general rule is "the first exception wins", so the first thrown exception is exposed to the handler. -But that may cause lost exceptions, for example if coroutine throws an exception in its `finally` block. -So, additional exceptions are suppressed. - -> One of the solutions would have been to report each exception separately, -but then [Deferred.await] should have had the same mechanism to avoid behavioural inconsistency and this -would cause implementation details of a coroutines (whether it had delegated parts of its work to its children or not) -to leak to its exception handler. - +When multiple children of a coroutine fail with an exception the +general rule is "the first exception wins", so the first exception gets handed. +All additional exceptions that happen after the first one are attached to the first exception as suppressed ones. @@ -301,7 +299,7 @@ Caught java.io.IOException with suppressed [java.lang.ArithmeticException] > Note, this mechanism currently works only on Java version 1.7+. Limitation on JS and Native is temporary and will be fixed in the future. -Cancellation exceptions are transparent and unwrapped by default: +Cancellation exceptions are transparent and are unwrapped by default:
@@ -312,13 +310,13 @@ import java.io.* fun main() = runBlocking { //sampleStart 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 } } } @@ -326,7 +324,7 @@ fun main() = runBlocking { 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() @@ -342,25 +340,26 @@ The output of this code is: ```text Rethrowing CancellationException with original cause -Caught original java.io.IOException +CoroutineExceptionHandler got java.io.IOException ``` ### Supervision As we have studied before, cancellation is a bidirectional relationship propagating through the whole -coroutines hierarchy. But what if unidirectional cancellation is required? +hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required. 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 have failed, it is not always necessary to cancel (effectively kill) the whole UI component, -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. +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. Another example is a server process that spawns several children jobs and needs to _supervise_ their execution, tracking their failures and restarting just those children jobs that had failed. #### Supervision job -For these purposes [SupervisorJob][SupervisorJob()] can be used. It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated +For these purposes [SupervisorJob][SupervisorJob()] can be used. +It is similar to a regular [Job][Job()] with the only exception that cancellation is propagated only downwards. It is easy to demonstrate with an example:
@@ -414,8 +413,8 @@ Second child is cancelled because supervisor is cancelled #### Supervision scope -For *scoped* concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation -only in one direction and cancels all children only if it has failed itself. It also waits for all children before completion +For _scoped_ concurrency [supervisorScope] can be used instead of [coroutineScope] for the same purpose. It propagates cancellation +in one direction only and cancels all children only if it has failed itself. It also waits for all children before completion just like [coroutineScope] does.
@@ -463,8 +462,11 @@ Caught assertion error #### Exceptions in supervised coroutines Another crucial difference between regular and supervisor jobs is exception handling. -Every child should handle its exceptions by itself via exception handling mechanisms. +Every child should handle its exceptions by itself via exception handling mechanism. This difference comes from the fact that child's failure is not propagated to the parent. +It means that coroutines launched directly inside [supervisorScope] _do_ use the [CoroutineExceptionHandler] +that is installed in their scope in the same way as root coroutines do +(see [CoroutineExceptionHandler](#coroutineexceptionhandler) section for details).
@@ -474,7 +476,7 @@ import kotlinx.coroutines.* fun main() = runBlocking { val handler = CoroutineExceptionHandler { _, exception -> - println("Caught $exception") + println("CoroutineExceptionHandler got $exception") } supervisorScope { val child = launch(handler) { @@ -496,7 +498,7 @@ The output of this code is: ```text Scope is completing Child throws an exception -Caught java.lang.AssertionError +CoroutineExceptionHandler got java.lang.AssertionError Scope is completed ``` diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt index 34d7b68c82..e08ddd0811 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-01.kt @@ -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 } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt index 5242ca1a00..67fdaa7177 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-02.kt @@ -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) { // root coroutine, running in GlobalScope throw AssertionError() diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt index e1fc22d725..9c9b43d22e 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-04.kt @@ -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 diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt index e97572aba8..04f9385f06 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-05.kt @@ -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) } diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt index eec27840e5..5a5b276bc3 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-exceptions-06.kt @@ -10,13 +10,13 @@ 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 } } } @@ -24,7 +24,7 @@ fun main() = runBlocking { 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() diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt index b32a004639..5efbe69146 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-supervision-03.kt @@ -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) { diff --git a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt index 4a140208f9..21d2c73b1b 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/test/ExceptionsGuideTest.kt @@ -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" ) } @@ -41,14 +41,14 @@ 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]" ) } @@ -56,7 +56,7 @@ class ExceptionsGuideTest { 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" ) } @@ -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" ) }