Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

8. Exception Handling
Written by Luka Kordić

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 launch coroutine 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’s Thread.UncaughExceptionHandler.
  • When async is used as a root coroutine builder, exceptions are thrown only when you call await().

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:

  1. You have to explicitly opt-in to use GlobalScope because it’s marked as DelicateCoroutinesApi. The IDE will warn you about this.
  2. You launch a coroutine using launch coroutine builder and you throw an IndexOutOfBoundsException in its body. This is an example of the normal exception propagation. The default implementation of Thread.UncaughExceptionHandlerhandles the exception. In this case, it simply prints the error as a part of the output.
  3. join() method makes the coroutine suspend and wait for the work to complete. In this case, launchJob completes with the exception.
  4. You launch a new coroutine by using async builder, and throw an ArithmeticException. In this case nothing is printed because the coroutine is launched via async. It relies on the user to call await().
  5. In order to catch and handle the exception, you call await() on the deferred object and wrap that in a try/catch block. 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 the println() 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.

@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)
  }
}
Caught java.lang.AssertionError: My Custom Assertion Error!

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.

@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")
      }
    }
  }
}
1. Throwing exception from async
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.

@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()
}
JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]

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.

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 suspendCancellableCoroutine { continuation ->
    getData(object : AsyncCallback {
      override fun onSuccess(result: String) {
        continuation.resume(result)
      }

      override fun onError(e: Exception) {
        continuation.resumeWithException(e)
      }
    })
  }
}
received: [Beep.Boop.Beep]
Caught IOException

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

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")
  }
}
Throwing exception in async
Caught java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException
fun main() = runBlocking {
  supervisorScope {
    val result = async {
      println("Throwing exception in async")
      throw IllegalStateException()
    }

    try {
      result.await()
    } catch (e: Exception) {
      println("Caught $e")
    }
  }
}
Throwing exception in async
Caught java.lang.IllegalStateException

Key Points

  • Exceptions thrown in launch coroutine builder are uncaught exceptions.
  • async coroutine builder encapsulates exceptions in the resulting Deferred object.
  • You can use regular Kotlin code in form of try/catch block to handle exceptions.
  • When using async, make sure to wrap the call to await in a try/catch block if you want to handle possible exceptions.
  • Add a CoroutineExceptionHandler to the parent coroutine context to catch uncaught exceptions.
  • 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 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now