Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 3.5

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

8. Exception Handling
Written by Nishant Srivastava

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

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.

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()
  }
}
1. Exception created via launch coroutine
Exception in thread "main" java.lang.IndexOutOfBoundsException
....
2. Caught java.lang.IndexOutOfBoundsException
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.

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

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

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.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now