Kotlin Coroutines in Android — Part 6

Exception propagation

Andrea Bresolin
Kin + Carta Created
5 min readApr 5, 2019

--

In standard sequential code, exceptions can be handled with try/catch and they propagate up in the calls stack until they’re caught. In case they’re not handled, they make the app crash. The propagation of exceptions within coroutines is basically the same, but there are a few differences that we’ll explore right now.

Catching exceptions thrown by coroutines

Keep in mind that coroutines can execute asynchronous tasks that all get in sync again after completion thanks to the suspend functions. The suspend functions calls are the points where an exception coming from a coroutine can be thrown and handled as well.

In case of a coroutine created by launch(), we can wait for its completion by calling join() on the Job that it returns. join() is a suspend function and that’s the point where any exception thrown by the coroutine will be propagated up to the call stack.

In case of async(), we have a Deferred that allows us to call await() to wait for the result of the asynchronous task executed in the new coroutine. await() is a suspend function as well and it’s the point where any exception thrown by the asynchronous task can be handled.

With any other suspend function, we can simply wrap the function call in try/catch to handle the exception like any other standard synchronous function.

Catching exceptions thrown by async coroutines

So far, handling exceptions with coroutines doesn’t look that complicated, but there’s a particular case that is not intuitive at all.

With the structured concurrency introduced in the stable version of the coroutines library, we have coroutine scopes. By default, if a coroutine within a scope fails with an exception that is not handled within the coroutine body, but propagated to the parent, all the coroutines within the same scope are expected to be cancelled and the scope is expected to fail. This is the default behaviour. To achieve this behaviour, in the current version of the coroutines library (version 1.1.1), whenever a coroutine created by async() throws an exception, the same exception is thrown both by its Deferred.await() and inside the body of the parent coroutine as well! This means that, no matter if we catch the exception by surrounding await() with try/catch, our app is still going to crash because the exception is not thrown just from the await() suspension point.

This behaviour is extremely confusing (if you want to follow some discussions about it, check https://github.com/Kotlin/kotlinx.coroutines/issues/763).

There are multiple proposed solutions on the web to avoid this issue and some of them don’t actually work as expected. Without going too much into the details, let’s take a look at a solution that actually works with a reasonable behaviour.

The proposed solution involves using supervisorScope. This suspend function creates a scope that doesn’t fail in case any of its children fail. To make the behavior consistent across all our coroutines, we’ll apply supervisorScope to both launch() and async().

Here is an example usage of launch() with supervisorScope:

And here is an example with async():

With the above changes, our app won’t crash anymore in case we catch an exception thrown by an async() coroutine and this works with any coroutine created by launch() as well. The reason is that any additional child coroutine created inside launch() or async() will always be enclosed in a supervisorScope, so any failure of a child coroutine won’t make the parent one fail. Let’s visualise this with some code:

The structure of the coroutines in the above example is the following:

The root launch() has two child async(). The second async() has a child launch(). Every launch() and async() encloses all its children within a supervisorScope. In this way, any failure of a child doesn’t make the parent coroutine fail.

By using supervisorScope, when a child coroutine fails, the parent coroutine doesn’t, so any other child coroutine will keep on running when a sibling one has failed. If we want to cancel the child coroutines when we catch an exception from one of them, we need to do it manually by calling cancel() on their corresponding Jobs or Deferreds as we’ve seen in the previous part of this series when we’ve been talking about cancellation.

What’s next

We’ve seen how the exception propagation works in the coroutines world and we’ve seen how tricky it can be in the current coroutines library version (1.1.1) when we have to deal with async(). At this point, you might ask yourself if there’s a simpler way of using the coroutines without having to write all the above code every time. This will be the topic of the next part where we’ll see how we can write a simple DSL (Domain-Specific Language) to easily work with coroutines for Android apps development.

Get the source code

The source code for this series can be found on GitHub. It’s an example Android project that covers multiple cases. Download it and play with it.

Missed the other parts of this series?

If you’ve missed the other parts of the Kotlin Coroutines in Android series, take a look at the introduction and check the full list of topics.

--

--