8.
Exception Handling
Written by Luka Kordić
Exception and error handling is an integral part of asynchronous programming. Imagine that you start an asynchronous operation, it runs through without any error and finishes with the result. That’s an ideal case. What if an error occurred during the execution? As with any unhandled exception, the application would crash. You may set yourself up for failure if you assume that any asynchronous operation is going to run through without any errors.
Before you can understand error and exception handling during a coroutine execution, it is important that you have an understanding of how these errors and exceptions are propagated through the process.
Exception Propagation
Exception handling is rather straightforward in coroutines. If the code throws an exception, the environment will propagate it without you having to do anything. Coroutines make asynchronous code look synchronous. Thus, you can use the same try/catch block to handle exceptions, like in the synchronous code.
You can build a coroutine in multiple ways. The kind of coroutine builder you use dictates how exceptions will propagate and how you can handle them.
- When using
launchcoroutine builder, exceptions are thrown as soon as they happen and are propagated up to the parent. Exceptions are treated as uncaught exceptions, similar to Java’sThread.UncaughExceptionHandler. - When
asyncis used as a root coroutine builder, exceptions are thrown only when you callawait().
Understanding how exceptions are propagated helps to figure out the right strategy for handling them.
Let’s code a simple example that creates new coroutines in GlobalScope and throws exceptions from different coroutine builders. To start, navigate to kco-materials/08-exception-handling/projects/starter directory and open the ExceptionHandling project in IntelliJ. Open up the CoroutineExceptionHandlingExample.kt and replace the code inside with the following code:
// 1
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// 2
val launchJob = GlobalScope.launch {
println("1. Exception created via launch coroutine")
throw IndexOutOfBoundsException()
}
// 3
launchJob.join()
println("2. Joined failed job")
// 4
val deferred = GlobalScope.async {
println("3. Exception created via async coroutine")
throw ArithmeticException()
}
// 5
try {
deferred.await()
println("4. Unreachable, this statement is never executed")
} catch (e: Exception) {
println("5. Caught ${e.javaClass.simpleName}")
}
}
Output:
1. Exception created via launch coroutine
2. Joined failed job
3. Exception created via async coroutine
5. Caught ArithmeticException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
— - -
Let’s break down the code from the example:
- You have to explicitly opt-in to use
GlobalScopebecause it’s marked asDelicateCoroutinesApi. The IDE will warn you about this. - You launch a coroutine using
launchcoroutine builder and you throw anIndexOutOfBoundsExceptionin its body. This is an example of the normal exception propagation. The default implementation ofThread.UncaughExceptionHandlerhandles the exception. In this case, it simply prints the error as a part of the output. -
join()method makes the coroutine suspend and wait for the work to complete. In this case,launchJobcompletes with the exception. - You launch a new coroutine by using
asyncbuilder, and throw anArithmeticException. In this case nothing is printed because the coroutine is launched viaasync. It relies on the user to callawait(). - In order to catch and handle the exception, you call
await()on thedeferredobject and wrap that in atry/catchblock. Notice that the IDE helps you a bit here. It recognizes that you called await and that an exception has occured. Because of this it gives you a warning that theprintln()statement is unreachable.
Try to comment out the deferred.await() call and see what happens. I’ll spoil it for you - the exception is swallowed and 4. Unreachable, this statement is never executed is printed.
CoroutineExceptionHandler
In the last example, you saw that IndexOutOfBoundsException was printed to the console. You can customize that behavior by creating a custom CoroutineExceptionHandler, which serves as a generic catch block for the root coroutine and its children. CoroutineExceptionHandler is an element of a CoroutineContext. It’s similar to Thread.uncaughtExceptionHandler, meaning that you can’t recover from an exception by using it.
It’s important to note that CoroutineExceptionHandler catches only exceptions that were not handled in any other way, i.e. uncaught exceptions.
Note: On Android, uncaughtExceptionPreHandler is the global coroutine exception handler.
Normally, uncaught exceptions can only result from coroutines created using launch coroutine builder. A coroutine that was created using async catches all its exceptions and represents them in the resulting Deferred object.
Note: There are cases when
asyncwill propagate the exception up and you won’t be able to catch it in thecatchblock.
We’ll talk about that in a moment.
In your ExceptionHandling starter project navigate to GlobalExceptionHandler.kt and replace the empty main function with the following code:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// 1
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
// 2
val job = GlobalScope.launch(exceptionHandler) {
throw AssertionError("My Custom Assertion Error!")
}
// 3
val deferred = GlobalScope.async(exceptionHandler) {
// Nothing will be printed,
// relying on user to call deferred.await()
throw ArithmeticException()
}
// 4
// This suspends current coroutine until all given jobs are complete.
joinAll(job, deferred)
}
}
Output:
Caught java.lang.AssertionError: My Custom Assertion Error!
Here is the explanation of the code block:
- Implementing a global exception handler; i.e.
CoroutineExceptionHandler. This is where you define how to handle the uncaught exception. - Creating a simple coroutine using
launchcoroutine builder, that throws anAssertionErrorwith a custom message. - Creating a simple coroutine using
asynccoroutine builder, that throws anArithmeticException. -
joinAllis used to suspend the current coroutine until all given jobs are complete.
CoroutineExceptionHandler is useful when you want to have a global exception handler shared between coroutines, but if you want to handle exceptions for a specific coroutine in a different manner, you are required to provide the specific implementation. Let’s take a look how you can do that.
Note: CoroutineExceptionHandler is invoked only on exceptions which are not expected to be handled by the user, so registering it in async coroutine builder and the like of it has no effect.
Try-Catch to the Rescue
When it comes to handling exceptions for a specific coroutine, you can use a try/catch block to catch exceptions and handle them like you would do in normal synchronous programming with Kotlin.
There’s a catch though. Coroutines created with async coroutine builder can typically swallow exceptions if you’re not careful. If an exception is thrown during an execution of the async block, the exception is not thrown immediately. Instead, it will be thrown at the time you call await on the returned Deferred object. This behavior, if not taken into account, can lead to situations where no exceptions are ever tracked. On the other hand, deferring exception handling until a later point in time can also be a desired behavior, depending on the use case at hand.
Here is an example to demonstrate the same. Open TryCatch.kt in your project and replace the empty main function with the code below:
@OptIn(DelicateCoroutinesApi::class)
fun main() {
runBlocking {
// Set this to 'true' to call await on the deferred variable
val callAwaitOnDeferred = false
val deferred = GlobalScope.async {
// This statement will be printed with or without
// a call to await()
println("Throwing exception from async")
throw ArithmeticException("Something Crashed")
// Nothing is printed, relying on a call to await()
}
if (callAwaitOnDeferred) {
try {
deferred.await()
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
}
}
Output for the case in which callAwaitOnDeferred is set to false — i.e., no call to await is made:
1. Throwing exception from async
Output for the case in which callAwaitOnDeferred is set to true — i.e., call to await is made:
1. Throwing exception from async
2. Caught ArithmeticException
Handling Multiple Child Coroutine Exceptions
Having just a single coroutine is an ideal use case. In practice, you may have multiple coroutines with other child coroutines running under them. What happens if those child coroutines throw exceptions? This is where all this might become tricky. In this case, the general rule is the first exception wins. If you set a CoroutineExceptionHandler, it will manage only the first exception, suppressing all the others.
To demonstrate this, open up the ExceptionHandlingForChild.kt in your starter project for this chapter. Replace the empty main function with the code below.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed${exception.suppressed?.contentToString()}")
}
// Parent Job
val parentJob = GlobalScope.launch(handler) {
// Child Job 1
launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("${e.javaClass.simpleName} in Child Job 1")
} finally {
throw ArithmeticException()
}
}
// Child Job 2
launch {
delay(100)
throw IllegalStateException()
}
// Delaying the parentJob
delay(Long.MAX_VALUE)
}
// Wait until parentJob completes
parentJob.join()
}
Output:
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]
In the previous example:
- You define a
CoroutineExceptionHandlerto print the name of the first exception caught along with the suppressed ones that it obtains from thesuppressedproperty. - After this, you start a parent coroutine using the
launchcoroutine builder with the exception handler as the parameter. The parent coroutine contains a couple of child coroutines that you launch using again thelaunchfunction. The first coroutine contains atry-catch-finallyblock. - In the
tryblock, you invoke thedelayfunction with a huge parameter value in order to wait for a long time. - In the
catchblock, you print a message about the caught exception. - With
finally, you throw anArithmeticException. - In the second coroutine, you
delayjust some milliseconds and then throw anIllegalStateException. - You then complete the parent coroutine, invoking the
delayfunction for another long period of time. - The last instruction of the
mainfunction allows the program to wait for the completion of the parent job.
When you run this code, the parent coroutine starts and so do its children. The first child waits and the second throws an IllegalStateException, which is the first exception that the handler will manage as you can see in the output. Because of this, the system forces the delay of the first coroutine to be canceled and this is the reason for the JobCancellationException message. This also makes the parent Job fail and, so, the handler will be invoked and its output displayed.
It’s important to note that the CoroutineExceptionHandler is a part of the parent coroutine and so it manages exceptions related to it.
Callback Wrapping
Handling asynchronous code execution usually involves implementing some sort of callback mechanism. For example, with an asynchronous network call, you probably want to have onSuccess and onFailure callbacks so that you can handle the two cases appropriately.
Such code can often become quite complex and hard to read. Luckily, coroutines provide a way to wrap callbacks to hide the complexity of the asynchronous code handling away from the caller via a suspendCancellableCoroutine suspending function, which is included in the coroutine library. It captures the current continuation instance and suspends the currently running coroutine.
The Continuation object provides two functions that you can use to resume the coroutine execution. Invoking the resume() method resumes the coroutine execution and returns a value, while resumeWithException() re-throws the exception right after the last suspension point.
Resuming is done by scheduling a call to one of the Continuation<T> methods in the future, inside a suspending function.
Take a look at an example of a simple long-running job with a callback for handling the result. You’re going to wrap the callback in a coroutine and simplify the job significantly. Open up the CallbackWrapping.kt in your starter project. You should find some pre-baked code there - a function that simulates a long-running task, and an AsyncCallback that you’re going to wrap. To complete the example add the following code to the main() function.
fun main() {
runBlocking {
try {
val data = getDataAsync()
println("Data received: $data")
} catch (e: Exception) {
println("Caught ${e.javaClass.simpleName}")
}
}
}
Below the main() function, add this piece of code:
// Callback Wrapping using Coroutine
suspend fun getDataAsync(): String {
return suspendCancellableCoroutine { continuation ->
getData(object : AsyncCallback {
override fun onSuccess(result: String) {
continuation.resume(result)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}
Output:
- When
triggerErrorfield is set tofalseingetData()method:
received: [Beep.Boop.Beep]
- When
triggerErrorfield is set totrueingetData()method:
Caught IOException
Great thing about the example above is that you’ve effectively made the bridge between async code using callbacks and the async code using coroutines. By doing this you get a nice, readable code that looks synchronous, but actually still does the async work under the hood.
Supervising Coroutines
Up until this point, we’ve talked about how the exceptions are propagated up in the hierarchy of coroutines. If a child coroutine throws an exception, it’s going to get propagated up to the root coroutine. But what happens when that behavior isn’t wanted? For example, there might be a UI component with its own scope. If you were to create a child coroutine inside of that scope, and that coroutine fails, UI component must not get cancelled. But, if the UI component’s scope is cancelled, that should also cancel all child jobs. Turns out, there are tools for those cases in the coroutines toolbox - supervisorJob and supervisorScope.
SupervisorJob
SupervisorJob is similar to a regular Job, the only difference being that the cancellation is propagated only downwards. Meaning that child coroutines that throw exceptions, won’t cancel their parent. Let’s have a look at an example. Navigate to SupervisorJob.kt. There’s an empty main function in there. Replace it with the following code:
fun main() = runBlocking {
// 1
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// 2
val firstChild = launch {
println("First child throwing an exception")
throw ArithmeticException()
}
// 3
val secondChild = launch {
println("First child is cancelled: ${firstChild.isCancelled}")
try {
delay(5000)
} catch (e: CancellationException) {
println("Second child cancelled because supervisor got cancelled.")
}
}
// 4
firstChild.join()
println("Second child is active: ${secondChild.isActive}")
supervisor.cancel()
secondChild.join()
}
}
Output:
First child throwing an exception
First child is cancelled: true
Second child is active: true
Second child cancelled because supervisor got cancelled.
Exception in thread "main" java.lang.ArithmeticException ...
Here’s a breakdown of the code above:
- You create an instace of
SupervisorJoband create a newCoroutineScopewith thatJobas a part of its context. - You create the first coroutine that throws
ArithmeticException(). - You create the second coroutine that prints the
cancelledstatus of the first one, delays its work for 5 seconds and catches aCancellationExceptionwhich is thrown whensupervisoris cancelled. - You wait for the completion of the first job. Then, you print the second child’s status to make sure that it is still active. After that, you cancel the
supervisorand wait for the completion of the second child. At this point,catchblock insecondChildwill trigger because of theCancellationException.
SupervisorScope
Remember the note from this chapter that said that async block will sometimes propagate the exception upwards, and you won’t be able to catch it? Let’s examine that case now, and see how we can use SupervisorScope to change that behavior. Open up the SupervisorScope.kt in the starter project and put in the following code:
fun main() = runBlocking {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
Output:
Throwing exception in async
Caught java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException
As you can see from the output, even though you caught the exception in the catch block, it’s still been propagated upwards and re-thrown. To fix this, you should use supervisorScope. It’s just a suspend function that creates a new CoroutineScope and runs the provided suspend block in that scope. The new scope is created with SupervisorJob, which means that child coroutines won’t affect each other upon failure.
Here’s the same code with the fix applied:
fun main() = runBlocking {
supervisorScope {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
}
Output:
Throwing exception in async
Caught java.lang.IllegalStateException
As you can see now, the exception isn’t propagated upwards and re-thrown. That’s because the supervisorScope lets coroutines handle exceptions themselves, instead of propagating them to the parent.
Key Points
- Exceptions thrown in
launchcoroutine builder are uncaught exceptions. -
asynccoroutine builder encapsulates exceptions in the resultingDeferredobject. - You can use regular Kotlin code in form of try/catch block to handle exceptions.
- When using
async, make sure to wrap the call toawaitin a try/catch block if you want to handle possible exceptions. - Add a
CoroutineExceptionHandlerto the parent coroutine context to catch uncaught exceptions. -
CoroutineExceptionHandleris invoked only on exceptions that are not expected to be handled by the user; registering it in an async coroutine builder or the like of it has no effect. - When multiple children of a coroutine throw an exception, the general rule is the first exception wins.
- Coroutines provide a way to wrap callbacks to hide the complexity of the asynchronous code handling away from the caller via a suspendCancellableCoroutine suspending function, which is included in the coroutine library.
- If you don’t want to propagate exceptions from child coroutines to the parent, use
SupervisorJob.
Where to Go From Here?
Exception handling is a crucial step in working with asynchronous programming. If the basics are not clear, it makes the process of programming and dealing with various asynchronous tasks pretty complex. Thankfully, when it comes to coroutines, you are now well versed with the concepts and implementations.
Next up, you will explore cancelling coroutines, so as to be able to stop them from executing when required. Exception handling and cancellation go hand-in-hand in coroutines. You’ll learn why in the next chapter.