article banner

Exception handling in Kotlin Coroutines

This is a chapter from the book Kotlin Coroutines. You can find it on LeanPub or Amazon.

An important part of how coroutines behave is their exception handling. Just as a program breaks when an uncaught exception slips by, a coroutine breaks in the case of an uncaught exception. This behavior is nothing new: for instance, threads also end in such cases. The difference is that coroutine builders also cancel their parents, and each cancelled parent cancels all its children. Let's look at the example below. Once a coroutine receives an exception, it cancels itself and propagates the exception to its parent (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 (runBlocking rethrows the exception).

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

Adding additional launch coroutines wouldn't change anything. Exception propagation is bi-directional: the exception is propagated from child to parent, and when those parents are cancelled, they cancel their children. Thus, if exception propagation is not stopped, all coroutines in the hierarchy will be cancelled.

Stop breaking my coroutines

Catching an exception before it breaks a coroutine is helpful, but any later is too late. Communication happens via a job, so wrapping a coroutine builder with a try-catch is not helpful at all.

import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking //sampleStart fun main(): Unit = runBlocking { // Don't wrap in a try-catch here. It will be ignored. try { launch { delay(1000) throw Error("Some error") } } catch (e: Throwable) { // nope, does not help here println("Will not be printed") } launch { delay(2000) println("Will not be printed") } } // Exception in thread "main" java.lang.Error: Some error... //sampleEnd

SupervisorJob

The most important way to stop coroutines breaking is by using a SupervisorJob. This 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).

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) } // Exception... // Will be printed //sampleEnd

A common mistake is to use a SupervisorJob as an argument to a parent coroutine, like in the code below. It won't help us handle exceptions, because in such a case SupervisorJob has only one direct child, namely the launch defined at 1 that received this SupervisorJob as an argument. So, in such a case there is no advantage of using SupervisorJob over Job (in both cases, the exception will not propagate to runBlocking because we are not using its job).

import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { // Don't do that, SupervisorJob with one children // and no parent works similar to just Job launch(SupervisorJob()) { // 1 launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will not be printed") } } delay(3000) } // Exception... //sampleEnd

It would make more sense if we used the same job as a context for multiple coroutine builders because each of them can be cancelled, but they won’t cancel each other.

import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { val job = SupervisorJob() launch(job) { delay(1000) throw Error("Some error") } launch(job) { delay(2000) println("Will be printed") } job.join() } // (1 sec) // Exception... // (1 sec) // Will be printed //sampleEnd

supervisorScope

Another way to stop exception propagation is to wrap coroutine builders with supervisorScope. This is very convenient as we still keep a connection to the parent, yet any exceptions from the coroutine will be silenced. supervisorScope behaves just like coroutineScope, but it uses a SupervisorJob instead of a regular Job.

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

supervisorScope is often used to start a number of independent coroutines.

suspend fun notifyAnalytics(actions: List<UserAction>) = supervisorScope { actions.forEach { action -> launch { notifyAnalytics(action) } } }

Beware, that coroutineScope cannot be replaced with withContext(SupervisorJob())! It 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

The problem here is that 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 here SupervisorJob is a parent of withContext coroutine. When a child has an exception, it propagates to coroutine coroutine, cancels its Job, cancels children, and throws an exception. The fact that SupervisorJob is a parent changes nothing.

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) } } } }

coroutineScope for exception handling

Another way to stop exception propagation is to use coroutineScope. Instead of influencing a parent, this function throws an exception that can be caught using try-catch (in contrast to coroutine builders). This way we ignore exceptions from coroutines, but also cancel other coroutines.

import kotlinx.coroutines.* //sampleStart fun main(): Unit = runBlocking { try { coroutineScope { launch { delay(1000) throw Error("Some error") } launch { delay(2000) println("Will be printed") } launch { delay(2000) println("Will be printed") } } } catch (e: Throwable) { println("Caught $e") } println("Done") } // (1 sec) // Caught java.lang.Error: Some error // Done //sampleEnd

Await

So, we know how to stop exception propagation, but sometimes this is not enough. In the case of an exception, the async coroutine builder breaks its parent, just like launch and other coroutine builders that have a relation with their parents. However, what if this process is silenced (for instance, using SupervisorJob or supervisorScope) and await is called? Let's look at the following example:

import kotlinx.coroutines.* //sampleStart class MyException : Throwable() suspend fun main() = 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

We have no value to return since the coroutine ended with an exception, so instead the MyException exception is thrown by await. This is why MyException is printed. The other async finishes uninterrupted because we’re using the supervisorScope.

Coroutine exception handler

When dealing with exceptions, sometimes it is useful to define default behavior for all of them. This is where the 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 exception (by default, it prints the exception stack trace).

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

This context is useful on many platforms to add a default way of dealing with exceptions. For Android, it often informs the user about a problem by showing a dialog or an error message.

Summary

Exception handling is an important part of the kotlinx.coroutines library. Over time, we will inevitably come back to these topics. For now, I hope that you understand how exceptions propagate from child to parent in basic builders, and how they can be stopped. Now it’s time for a long-awaited, strongly connected topic. Time to talk about coroutine scope functions.