@@ -406,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.
@@ -455,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).
@@ -466,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) {
@@ -488,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
```
@@ -501,6 +511,8 @@ Scope is completed
[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html
[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
+[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
+[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
[Job.cancel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
[SupervisorJob()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
diff --git a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
index cd7fd0d7ca..b49a6faa35 100644
--- a/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
+++ b/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
@@ -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
@@ -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 {
/**
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 359eff60e4..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,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)
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"
)
}