Concurrency with Coroutines in Android

Jun 5 2024 · Kotlin 1.9.23, Android 14, Android Studio Iguana

Lesson 05: Handle Errors

Demo

Episode complete

Play next episode

Next
Transcript

Open the starter project in Android Studio. Go to the Lesson5Screen and look at the code. On top, you have the coroutine scope you will use in this lesson. Its context contains several elements.

The first is a CoroutineName. As the name suggests, it’s the name of the coroutine for debugging purposes. You’ll see an example in the name of the thread when logging.

The next element is a SupervisorJob(). It’s here to not cancel the parent coroutine and other siblings when one of the child coroutines fails. Clicking the buttons will cause the exceptions in the child coroutines. So, if there was a plain Job() instead of the SupervisorJob() after each click, the scope will become unusable. You’d have to restart the application to make it work again.

Finally, there’s a CoroutineExceptionHandler. It’s here to catch the exceptions that aren’t caught in the coroutines. You’ll see the logs in the logcat. Note that the exceptions are not rethrown after logging. If they were rethrown, the application would crash.

Your first task is to throw an exception from the launch coroutine builder. Go to the first button, click handler and insert the following code there:

coroutineScope.launch {
  Log.d(
    "Lesson5",
    "Coroutine inside launch on thread: ${Thread.currentThread().name}",
  )
  throw Exception("launch went wrong")
}

and import the launch extension. Launch the application and click the first (“Throw exception from launch”) button. You should see the log in the logcat. Note the log entry with the coroutine name inside the thread name. Note the number after the coroutine name. It is the unique, automatically incremented number of the coroutine.

They’re generated if coroutine debugging is turned on. You can turn it on by setting the DEBUG_PROPERTY_NAME system property to the value of DEBUG_PROPERTY_VALUE_ON like this:

System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)

See the top of the MainActivity class for the full example.

After the log entry from inside the coroutine, you should see the next entry with the exception message. It comes from the CoroutineExceptionHandler. It shows that the exception wasn’t thrown by the launch builder invocation. If it was, the application would crash because the launch happens in the main thread.

Click the first button again. You should see the same logs. The SupervisorJob() in the context of the scope prevents the parent coroutine from being cancelled. So, the scope is still usable.

Your second task is to throw an exception from the runBlocking coroutine builder. Go to the second button, click handler and insert the analogous code there as in the first task:

runBlocking {
  Log.d(
    "Lesson5",
    "Coroutine inside runBlocking on thread: ${Thread.currentThread().name}",
  )
  throw Exception("runBlocking went wrong")
}

and import the runBlocking extension. Run the application and click the second (“Throw exception from runBlocking”) button. Look at the logcat.

After the message with a thread name, you should see the log entry with the exception message. The app crashed. That is because the runBlocking builder is synchronous from the caller’s perspective. The runBlocking isn’t bound to a coroutineScope. It’s a top-level function. So, the uncaught exception handler of the scope doesn’t catch the exception. The exception is propagated to the uncaught exception handler of the Android main thread.

That default uncaught exception handler of the thread terminates the application.

Your next task is to throw an exception from the async coroutine builder. Go to the third button, click handler and add a code analogous to the previous tasks:

coroutineScope.async {
  Log.d(
    "Lesson5",
    "Coroutine inside async on thread: ${Thread.currentThread().name}",
  )
  throw Exception("async went wrong")
}

and import the async extension. Run the application and click the third button - “Throw exception from async”. Look at the logcat.

You should see the log entry with the thread name. So the async block gets executed. But there’s no log entry with the exception message. That is because exceptions from the async builders in root scopes are not propagated to the uncaught exception handler. The coroutineScope is the root scope in this case.

Your last task is to throw the aggregated exceptions. Go to the fourth button and click handler. This task is a bit more complex. First, think about how you can achieve multiple exceptions thrown at the same time. You may try something like this:

coroutineScope.launch { throw Exception("Exception 1") }
coroutineScope.launch { throw Exception("Exception 2") }

This won’t work. One of the throws will be executed a little bit earlier than the other. So the first exception will cancel the parent and the other child coroutine before it has a chance to throw the exception.

To overcome this problem, you have to throw the exception from one of the coroutines while the other is currently suspending. For example, you can use the following code:

coroutineScope.launch {
  launch {
    try {
      delay(Long.MAX_VALUE)
    } finally {
      throw IllegalArgumentException("Illegal argument exception")
    }
  }
  launch {
    delay(100)
    throw IOException("I/O exception")
  }
}

and add any imports. This is a way to throw an exception from one of the child coroutines while the other one is currently suspending. The suspension point has to be wrapped in the try-finally blocks. Then you can throw the exception from the finally block. The finally block is executed after the exception happens in the try block.

The exception will be thrown there when the other coroutine fails. That’s because if one of the children fails, it cancels the other siblings. The cancellation is done by throwing the CancellationException. This special kind of exception never reaches the uncaught exception handler.

Note that the coroutine throwing the exceptions aren’t the direct children of the root coroutineScope! There’s one more coroutine in between. That’s because the direct children of the root scope with the SupervisorJob() in the context aren’t canceled when one of them fails.

The delay in the second coroutine is necessary to make sure the first one reaches the delay function invocation before it gets canceled due to the exception thrown by the second coroutine.

Launch the application and click the fourth button - “Throw aggregated exceptions”. Look at the logcat. You should see a stacktrace with the suppressed exception.

See forum comments
Cinema mode Download course materials from Github
Previous: Instructions Next: Conclusion