
fetchUserDetails, which concurrently fetches user data and user preferences and then combines them into a single object. This is, of course, a simplification of an extremely popular use case, where a suspending function starts multiple asynchronous operations and then waits for their results.suspend fun fetchUserDetails(): UserDetails = coroutineScope { val userData = async { fetchUserData() } val userPreferences = async { fetchUserPreferences() } UserDetails(userData.await(), userPreferences.await()) }
fetchUserPreferences throws an exception. What should happen to the fetchUserDetails call? What about the coroutine that calls fetchUserData? I believe that the answer given to us by the kotlinx.coroutines library is the best one. fetchUserDetails should throw the exception that occurred in fetchUserPreferences, and the coroutine that calls fetchUserData should be cancelled. This is the default behavior of the kotlinx.coroutines library, but how does it work?- Suspending functions and synchronous coroutines, including coroutine scope functions and
runBlocking, throw exceptions that end their body. - Asynchronous coroutine builders (
asyncandlaunch) propagate exceptions that end their body to their parents via the scope. An exception received this way is treated as if it occurred in the parent coroutine.
fetchUserPreferences coroutine, i.e., inside async. It ends this coroutine builder body and therefore propagates to the parent of async, which is coroutineScope. This means that coroutineScope treats this exception like it occurred in its body, so it throws this exception; as a result, this exception is thrown from fetchUserDetails. coroutineScope also gets cancelled, so all its children are also cancelled. This is why the fetchUserData coroutine is cancelled.launch). The parent cancels itself and all its children, then it propagates the exception to its parent (runBlocking). runBlocking is a root coroutine (it has no parent), so it just ends the program by rethrowing this exception. This way, everything is cancelled, and program execution stops.import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking //sampleStart fun main(): Unit = runBlocking { launch { launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will not be printed") } launch { delay(500) // faster than the exception println("Will be printed") } } launch { delay(2000) println("Will not be printed") } } // Will be printed // Exception in thread "main" java.lang.Error: Some error... //sampleEnd

Cancellation of an asynchronous coroutine propagates to its parent, that treats it like its own exception. So it cancels its other children, and propagates it up to its parent. In this picture, Coroutine#6 has an exception, so it propagates Coroutine#3 and then to Coroutine#1. When those coroutines get cancelled, they cancel their children.
// Exception in fetchUserPreferences is ignored suspend fun fetchUserDetails(): UserDetails = coroutineScope { val userData = async { fetchUserData() } val userPreferences = async { try { fetchUserPreferences() } catch (e: Exception) { println("Error in fetchUserPreferences: $e") null } } UserDetails(userData.await(), userPreferences.await()) }
// Exception in fetchUserPreferences cancells fetchUserDetails, // and makes fetchUserData return null suspend fun fetchUserDetails(): UserDetails? = try { coroutineScope { val userData = async { fetchUserData() } val userPreferences = async { fetchUserPreferences() } UserDetails(userData.await(), userPreferences.await()) } } catch (e: Exception) { println("Error in fetchUserDetails: $e") null }
SupervisorJob instead of a regular Job. This typically applies to the supervisorScope coroutine scope function and to custom CoroutineScope with SupervisorJob as a context. So, let's talk about SupervisorJob.SupervisorJob is a special kind of job that ignores all exceptions in its children. SupervisorJob is generally used as part of a scope in which we start multiple coroutines (more about this in the Constructing coroutine scope chapter). Thanks to this, an exception in one coroutine will not cancel this scope and all its children.import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { val scope = CoroutineScope(SupervisorJob()) scope.launch { delay(1000) throw Error("Some error") } scope.launch { delay(2000) println("Will be printed") } delay(3000) println(scope.isActive) } // (1 sec) // Exception... // (2 sec) // Will be printed // true //sampleEnd
Job instead of SupervisorJob, the exception would propagate to the parent, the second coroutine would be cancelled, and the scope would end in the "Cancelled" state.
SupervisorJob as an argument to a parent coroutine, like in the code below. This won't help us handle exceptions. Remember that Job is the only context that is not inherited, so it becomes a parent when it is passed to a coroutine builder. In this case, SupervisorJob has only one direct child, namely the launch defined at 1 that received this SupervisorJob as an argument. Therefore, when an exception occurs in this child, it propagates to the parent, which uses a regular Job, so this coroutine cancels all its children. There is no advantage of using SupervisorJob over Job.import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { // DON'T DO THAT! launch(SupervisorJob()) { // 1 launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will not be printed") } } delay(3000) } // Exception... //sampleEnd
launch 1 propagates to runBlocking, which cancels launch 2 because it uses a regular Job internally. Then runBlocking throws this exception. Using SupervisorJob in this case changes absolutely nothing because it becomes a parent of runBlocking. Using SupervisorJob does not change the fact that runBlocking uses a regular Job.import kotlinx.coroutines.* //sampleStart // DON'T DO THAT! fun main(): Unit = runBlocking(SupervisorJob()) { launch { // 1 delay(1000) throw Error("Some error") } launch { // 2 delay(2000) println("Will not be printed") } } // Exception... //sampleEnd
SupervisorJob is to use supervisorScope. It is a coroutine scope function, so it behaves just like coroutineScope, but it uses a SupervisorJob instead of a regular Job. This way, exceptions from its children are ignored (they only print stacktrace).import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { supervisorScope { launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will be printed") } launch { delay(2000) println("Will be printed") } } println("Done") } // (1 sec) // Exception... // Will be printed // Will be printed // Done //sampleEnd
coroutineScope instead of supervisorScope, the exception would propagate to runBlocking, and the program would end with an exception without printing anything.supervisorScope only ignores exceptions from its children. If an exception occurs in supervisorScope itself, it breaks this coroutine builder and the exception propagates to its parent. If an exception occurs in a child of a child, it propagates to the parent of this child, destroys it, and only then gets ignored.supervisorScope is often used when we need to start multiple independent processes and we don't want an exception in one of them to cancel the others.suspend fun notifyAnalytics(actions: List<UserAction>) = supervisorScope { actions.forEach { action -> launch { notifyAnalytics(action) } } }
supervisorScope does not support changing context. If you need to both change context and use a SupervisorJob, you need to wrap supervisorScope with withContext.suspend fun notifyAnalytics(actions: List<UserAction>) = withContext(dispatcher) { supervisorScope { actions.forEach { action -> launch { notifyAnalytics(action) } } } }
supervisorScope with withContext(SupervisorJob())! This is because Job cannot be set from the outside, and withContext always uses a regular Job.import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { // DON'T DO THAT! withContext(SupervisorJob()) { launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will be printed") } launch { delay(2000) println("Will be printed") } } delay(1000) println("Done") } // (1 sec) // Exception... //sampleEnd
Job is the only context that is not inherited. Each coroutine needs its own job, and passing a job to a coroutine makes it a parent. So, SupervisorJob here is a parent of the withContext coroutine. When a child has an exception, it propagates to the withContext coroutine, cancels its Job, cancels its children, and throws an exception. The fact that SupervisorJob is a parent changes nothing.
await on Deferred, it should return the result of this coroutine if the coroutine finished successfully, or it should throw an exception if the coroutine ended with an exception (i.e., CancellationException if the coroutine was cancelled). This is why if we want to silence exceptions from async, it is not enough to use supervisorScope: we also need to catch the exception when calling await.import kotlinx.coroutines.* //sampleStart class MyException : Throwable() suspend fun main(): Unit = supervisorScope { val str1 = async<String> { delay(1000) throw MyException() } val str2 = async { delay(2000) "Text2" } try { println(str1.await()) } catch (e: MyException) { println(e) } println(str2.await()) } // MyException // Text2 //sampleEnd
CoroutineExceptionHandler context comes in handy. It does not stop the exception propagating, but it can be used to define what should happen in the case of an uncaught exception (the default behavior is that exception stacktrace is printed).import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { val handler = CoroutineExceptionHandler { ctx, exception -> println("Caught $exception") } val scope = CoroutineScope(SupervisorJob() + handler) scope.launch { delay(1000) throw Error("Some error") } scope.launch { delay(2000) println("Will be printed") } delay(3000) } // Caught java.lang.Error: Some error // Will be printed //sampleEnd
CoroutineExceptionHandler handler will only be invoked for the root coroutines created using the launch builder, and coroutines started in supervisorScope. Regular children coroutines 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. A coroutine that was created using async always catches all its exceptions and represents them in the resulting Deferred object, so it cannot result in uncaught exceptions. Coroutines running with SupervisorJob do not propagate exceptions to their parent and are treated like root coroutines.CoroutineExceptionHandler context can be useful to add a default way of dealing with exceptions. On Android, it is often used to provide a default way of handling exceptions in the UI layer. In general, it can also be used to specify a default way of logging exceptions.val handler = CoroutineExceptionHandler { _, exception -> Log.e("CoroutineExceptionHandler", "Caught $exception") }
- Exceptions propagate from child to parent. Suspending functions (including coroutine scope functions) throw exceptions, and asynchronous coroutine builders propagate exceptions to their parents via the scope.
- To stop exception propagation, you can catch exceptions from suspending functions before they reach coroutine builders, or catch exceptions from scope functions.
SupervisorJobis a special kind of job that ignores all exceptions in its children. It is used to prevent exceptions from canceling all the children of a scope.SupervisorJobshould not be used as a builder argument as it does not change the fact that the parent uses a regularJob.supervisorScopeis a coroutine scope function that uses aSupervisorJobinstead of a regularJob. It ignores exceptions from its children.withContext(SupervisorJob())is not a good replacement forsupervisorScopebecauseSupervisorJobis not inherited, andwithContextalways uses a regularJob.- When calling
awaitonDeferred, it should return the value if the coroutine finished successfully, or it should throw an exception if the coroutine ended with an exception. CoroutineExceptionHandleris a context that can be used to define default behavior for all exceptions in a coroutine.