8.
Exception Handling
Written by Nishant Srivastava
Exception and error handling is an integral part of asynchronous programming. Imagine that you initiate 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 normally crash. You may set yourself up for failure if you assume that any asynchronous operation is going to run through successfully without any error.
Before you can understand error and exception handling during coroutine execution, it is important that you have an understanding of how these errors and exceptions are propagated through the process.
Exception propagation
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 launch and actor coroutine builders, exceptions are propagated automatically and are treated as unhandled, similar to Java’s
Thread.uncaughExceptionHandler. - When using async and produce coroutine builders, exceptions are exposed to the users to be consumed finally at the end of the coroutine execution via await or receive.
Understanding how exceptions are propagated helps to figure out the right strategy for handling them.
Handling exceptions
Exception handling is pretty straightforward in coroutines. If the code throws an exception, the environment will automatically propagate it and you don’t have to do anything. Coroutines make asynchronous code look synchronous, similar to the expected way of handling synchronous code — i.e., try-catch applies to coroutines, too.
Here is a simple example that creates new coroutines in GlobalScope and throws exceptions from different coroutine builders:
fun main() = runBlocking {
val asyncJob = GlobalScope.launch {
println("1. Exception created via launch coroutine")
// Will be printed to the console by
// Thread.defaultUncaughtExceptionHandler
throw IndexOutOfBoundsException()
}
asyncJob.join()
println("2. Joined failed job")
val deferred = GlobalScope.async {
println("3. Exception created via async coroutine")
// Nothing is printed, relying on user to call await
throw ArithmeticException()
}
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
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
- - -
2. Joined failed job
3. Exception created via async coroutine
4. Caught ArithmeticException
Note: You can find the executable version of the above snippet of code in the starter project in the file called CoroutineExceptionHandlingExample.kt.
In the previous code, you launch a coroutine using the GlobalScope.launch coroutine builder and you throw an IndexOutOfBoundsException in its body. This is an example of the normal exception propagation which is handled by the default Thread.uncaughExceptionHandler implementation. This is the object responsible for managing the unhandled exceptions thrown in the application. It just propagates the exceptions to the caller’s thread handler, if any, or prints their message on the standard output. In this case, you’re into the main function so the error message is part of the output.
As you know, the GlobalScope.launch creates a Job instance and you invoke the join function on it. The first job, because of the exception, completes so the println() methond that prints 2. Joined failed job is also part of the output. In the second coroutine, you use the GlobalScope.async coroutine builder which throws an ArithmeticException into its body. In this case, the exception is not handled by the Thread.uncaughExceptionHandler the moment it’s been created, but can be thrown by the await function invoked on the Deferred object that the GlobalScope.async returns.
In this case, also the possible exception is deferred in time.
CoroutineExceptionHandler
Similar to using Java’s Thread.defaultUncaughtExceptionHandler, which returns a handler for uncaught thread exceptions, coroutines offer an optional and generic catch block to handle uncaught exceptions called CoroutineExceptionHandler.
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 always catches all its exceptions and represents them in the resulting Deferred object.
When using the launch builder, the exception will be stored in a Job object. To retrieve it, you can use the invokeOnCompletion helper function:
fun main() {
runBlocking {
val job = GlobalScope.launch {
println("1. Exception created via launch coroutine")
// Will NOT be handled by
// Thread.defaultUncaughtExceptionHandler
// since it is being handled later by `invokeOnCompletion`
throw IndexOutOfBoundsException()
}
// Handle the exception thrown from `launch` coroutine builder
job.invokeOnCompletion { exception ->
println("2. Caught $exception")
}
// This suspends coroutine until this job is complete.
job.join()
}
}
Output:
1. Exception created via launch coroutine
Exception in thread "main" java.lang.IndexOutOfBoundsException
....
2. Caught java.lang.IndexOutOfBoundsException
Note: You can find the executable version of the above snippet of code in the starter project in the file called ExceptionHandlingForLaunch.kt.
By default, when you don’t set a handler, the system handles uncaught exceptions in the following order:
- If the exception is
CancellationExceptionthen the system ignores it because that is the mechanism to cancel the running coroutine. - Otherwise, if there is a
Jobin the context, thenJob.cancelis invoked. - Otherwise, all instances of
CoroutineExceptionHandlerfound via ServiceLoader and current thread’sThread.uncaughtExceptionHandlerare invoked.
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.
Here is a simple example to demonstrate the usage of CoroutineExceptionHandler:
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!
Note: You can find the executable version of the above snippet of code in the starter project in the file called GlobalExceptionHandler.kt
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 exception when one is thrown from an unhandled coroutine. - Creating a simple coroutine using
launchcoroutine builder, that throws a custom messageAssertionError - 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 us take a look at how.
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 is the catch though. Coroutines created with async coroutine builder can typically “swallow” exceptions if you’re not careful. If an exception is thrown during an async block, the exception is not actually thrown immediately. Instead, it will be thrown at the time you call await on the Deferred object that is returned. This behavior, if not taken into account, can lead to situations where no exceptions are ever tracked, but deferring exception handling until a later time can also be a desired behavior depending on the use case at hand.
Here is an example to demonstrate the same:
fun main() {
runBlocking {
// Set this to ’true’ to call await on the deferred variable
val callAwaitOnDeferred = true
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")
}
}
}
}
Note: You can find the executable version of the above snippet of code in the starter project in the file called TryCatch.kt
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 false — i.e., no 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.
Here is an example to demonstrate this:
fun main() = runBlocking {
// Global Exception Handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception with suppressed " +
// Get the suppressed exception
"${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()
}
Note: You can find the executable version of the above snippet of code in the starter project in the file called ExceptionHandlingForChild.kt.
Output:
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]
In the previous example:s
- You define a CoroutineExceptionHandler to print the name of the first exception caught along with the suppressed ones that it obtains from the suppressed property.
- 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 a try-catch-finally block. - In the try block, you invoke the
delayfunction with a huge parameter value in order to wait for a long time. - In the catch, you print a message about the caught exception.
- With finally, you throw an ArithmeticException.
- In the second coroutine, you
delayjust some milliseconds and then throw an IllegalStateException. - 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 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 suspendCoroutine 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 with which you can resume the coroutine execution. Invoking the resume function 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 calling to Continuation method in the future inside a suspending function.
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:
fun main() {
runBlocking {
try {
val data = getDataAsync()
println("Data received: $data")
} catch (e: Exception) {
println("Caught ${e.javaClass.simpleName}")
}
}
}
// Callback Wrapping using Coroutine
suspend fun getDataAsync(): String {
return suspendCoroutine { cont ->
getData(object : AsyncCallback {
override fun onSuccess(result: String) {
cont.resumeWith(Result.success(result))
}
override fun onError(e: Exception) {
cont.resumeWith(Result.failure(e))
}
})
}
}
// Method to simulate a long running task
fun getData(asyncCallback: AsyncCallback) {
// Flag used to trigger an exception
val triggerError = false
try {
// Delaying the thread for 3 seconds
Thread.sleep(3000)
if (triggerError) {
throw IOException()
} else {
// Send success
asyncCallback.onSuccess("[Beep.Boop.Beep]")
}
} catch (e: Exception) {
// send error
asyncCallback.onError(e)
}
}
// Callback
interface AsyncCallback {
fun onSuccess(result: String)
fun onError(e: Exception)
}
Note: You can find the executable version of the above snippet of code in the starter project in the file called CallbackWrapping.kt.
Output:
-
When
triggerErrorfield is set tofalseingetData()method:Data received: [Beep.Boop.Beep] -
When
triggerErrorfield is set totrueingetData()method:Caught IOException
Key points
- If an exception is thrown during an asynchronous block, it is not actually thrown immediately. Instead, it will be thrown at the time you call await on the Deferred object that is returned.
- To ignore any exceptions, launch the parent coroutine with the async function; however, if required to handle, the exception uses a try-catch block on the await() call on the Deferred object returned from async coroutine builder.
- When using launch builder the exception will be stored in a Job object. To retrieve it, you can use the invokeOnCompletion helper function.
- Add a CoroutineExceptionHandler to the parent coroutine context to catch unhandled exceptions and handle them.
- CoroutineExceptionHandler is invoked only on exceptions that are not expected to be handled by the user; registering it in an async coroutine builder or the like 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 suspendCoroutine suspending function, which is included in the coroutine library.
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.