Kotlin Coroutines: Fundamentals

Feb 14 2024 · Kotlin 1.9, Android 13, Android Studio Giraffe

Part 2: Deep Dive into Coroutines

10. Handle Exceptions in Coroutines

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 09. Deep Dive into Async/Await Next episode: 11. Challenge: Implement a Coroutine Bound to Composable with Exception Handling

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 10. Handle Exceptions in Coroutines

Welcome to the tenth episode of the course! This is the last episode before the final challenge! :]

You’ll learn about coroutine exception handlers, recovering the stacktraces and how to debug the coroutines.

You may recall that coroutines are asynchronous by default and that exceptions are propagated to the parent coroutine.

Failure of child coroutine may or may not fail the parent and other siblings.

Coroutines are asynchronous by default. The launch and async builders starts them in the background and return immediately.

So, the uncaught exceptions in coroutines won’t propagate to the builder caller. Wrapping those builders with a try-catch block is pointless.

On the other hand, the runBlocking builder is synchronous from the perspective of the caller. It blocks the caller thread until the coroutine finishes and optionally throws the exceptions.

What happens with the uncaught exceptions thrown inside the coroutines? Well, it depends on the several factors.

Let’s check the basic ones! Open the ErrorHandlingScreen file in Android Studio.

On the top you can see the scope with the SupervisorJob and the exception handler.

You’ve seen them in the previous episodes.

Now you’ll learn more about the exception handler and which exceptions it catches.

The screen contains several buttons launching coroutines with different properties.

There are also logs printing the current thread, which helps to understand what’s going on.

Look at the first button.

It launches the coroutine which fails. Run the app and click the first button.

Open the LogCat.

As you can see, the exception was caught by the handler.

Now, look at the second button.

It uses the runBlocking builder without any scope. Run the app and click the second button.

The app has crashed. That’s because there is no exception handler in the scope hierarchy running the coroutine.

In such cases the exception is propagated to the thread exception handler. By default it just terminates the process.

That seems to be intuitive. If there is an exception handler, the uncaught exceptions goes there. If there is no handler, the exception is propagated upwards.

Now, look at the click handler of the third button.

It differs from the first one only by using the async builder instead of the launch. Let’s try it out!

Open the LogCat.

There is a log proving that coroutine has started and then… nothing. Scope is the same, it has the exception handler. But, the exception disappeared.

It doesn’t look so intuitive now, right? The exceptions from coroutines of the async builders are not propagated to the exception handler if they are in the root (or top-level) scope.

What does root mean here? Let’s wrap the async with the launch and see what happens. Remove also the scope. prefix from the async.

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

Now the async is not in the root scope anymore. It’s a child of the launch coroutine. Run the app and click the third button.

Open the LogCat.

This time exception was caught by the handler.

In the previous case with async in the root scope the exception wasn’t swallowed. You could retrieve it by using the await() function.

It will throw any exception from the coroutine. Note, the uncaught exceptions always propagate to the parent coroutine.

Even if they don’t reach the exception handler, they will cause a failure of the parent coroutine. Unless it uses a SupervisorJob.

You can add the CoroutineExceptionHandlers to the root scopes. The handlers installed in non-root scopes won’t work.

The roots are the very first scopes in the hierarchy and those which has the SuperveisorJob as their jobs.

All the uncaught exceptions from the coroutines in the scope will be caught by the handler in the root scope.

But, keep in mind it the handlers are the last-resort mechanisms. You cannot recover from the exception using them.

The coroutines are already failed when the handler is invoked. You can only log the exception or send it to the crash reporting system.

The try-catch blocks works normally inside the coroutines. You can use them to recover from the exceptions.

But, you have to remember to rethrow the CancellationExceptions. Otherwise the coroutine may keep executing after cancellation.

What is more, coroutineScope builder which you have learned about in the seventh episode, also throws the uncaught exceptions from the child coroutines.

The supervisorScope on the other hand, won’t throw the exceptions from the child coroutines. They are treated as started in root scope.

The coroutines can be executed on the various threads. That makes debugging them a bit harder.

Fortunately, you can assign the names to coroutines. It will appear in the debugger.

Let’s try it out! Open the the ErrorHandlingScreen in Android Studio .

Add a CoroutineName element, with the name of the screen to the context:

val scope = CoroutineScope(
  CoroutineName("ErrorHandlingScreen") +
          SupervisorJob() + CoroutineExceptionHandler { _, throwable ->
      Log.e("ErrorHandlingScreen", "Exception handler", throwable)
  })

Run the app

Go to the Error Handling Screen.

Click the first button.

And open the LogCat.

As you can see, the name appeared in the logs. It’s very useful when you have many coroutines running at the same time.

In order for that to work, the coroutine debugging facilities must be turned on. In this project they are already enabled for demonstration purposes in the previous episodes.

But, in your own projects you have to enable them manually. Open the MainActivity file and check how it’s done.

At the very beginning of the class there is a companion object with the init block. It is executed at the very beginning, before the MainActivity is created.

Such code can be also placed in the Application class. It will be executed when every app process starts.

Remove the init block, run the app.

Go the Error Handling screen and click the first button.

Open the LogCat.

As you can see, the coroutine names are not there anymore.

OK, that’s all for this episode! And that was a last one before the challenge! :]

Take a break and prepare yourself for the final challenge!