Managing exceptions in nested coroutine scopes

Fabio Collini
ProAndroidDev
Published in
5 min readApr 1, 2019

--

Coroutine scopes are the latest concept introduced to the Kotlin coroutines library before the 1.0 release. Scopes are really useful to group together some coroutines and to drive their lifecycle (basically when they need to be cancelled). It’s quite easy to use them with suspended methods, unfortunately we need to pay attention to how to use them when nested scopes are involved.

Let’s see a practical example. A loadData method (defined as suspend) invokes three methods:

  • callServer lasts one second and fails when called with true as argument
  • loadPrefs lasts half a second and fails when called with true as argument
  • loadFallback is invoked only if one of the other two methods fail, it lasts 100 milliseconds and never fails

loadData is invoked inside a launch coroutine builder on a simulated viewModelScope, here the complete code:

Thanks to the Kotlin playground this code is executable, clicking on the play button you can execute it on the browser.

Executing the code we can see that the total time is around 1500 milliseconds, passing true to callServer or loadPrefs we can verify that loadFallback is invoked in the catch block. The code seems to be synchronous, thanks to the coroutines we don’t need to use callbacks or a different syntax to manage asynchronous code.

Let’s go async!

The example can be improved, the two methods loadPrefs and callServer can be executed in parallel to decrease the total execution time. Using the async coroutine builder we can launch a method in a background thread and obtain the result later invokingawait on the Deferred object. Our goal is to modify the loadData method in this way:

try {
val deferredResult = async { callServer() }
val
prefsValue = loadPrefs()
val serverValue = deferredResult.await()
prefsValue + serverValue
} catch (e: Exception) {
loadFallback()
}

The callServer is executed using an async coroutine builder, so it’s started in background. Then, the loadPrefs is executed in the usual way waiting for the result and, finally, the serverValue is obtained invoking await. In this way callServer and loadPrefs are executed in parallel.

Copying and pasting this code inside the method body is not enough because async is an extension method on the CoroutineScope interface. So we need a CoroutineScope inside the method which can be obtained in three ways:

  • using the viewModelScope defined in the class
  • defining the loadData as a CoroutineScope extension method
  • invoking the coroutineScope method

We can use the viewModelScope, but I personally find this solution confusing because the external launch and the internal async are invoked on the same scope. Another problem is that it can be used only if the method is in the same class: in a real example probably the loadData method should be defined in another class (a UseCase or a Repository for example). The second and the third solution are similar but there is an important difference: using a CoroutineScope extension method we reuse the same CoroutineScope of the caller method, invoking the coroutineScope method we create a new nested scope.

There are two important things to know about nested scopes:

  • the coroutines executed in this new scope are grouped together, the scope fails (and the method throws an exception) when one of them fails
  • the method doesn’t terminate until all the coroutines are terminated, this is really important if the method creates a channel

So let’s modify the loadData method wrapping all the body inside a coroutineScope invocation:

Executing this code we can verify that the total execution time is shorter. But what about error management? The try/catch is still there so it should work, but… executing the following code that simulates an error in the callServer method we can see something strange:

An exception is thrown and the updateUi method is never executed. Executing this code on a real ViewModel on Android the result is even worse, the app will crash!

The problem is that the exception inside the async block causes a failure in the scope created by coroutineScope. So, even if the exception is caught, the scope is in an invalid state and it will rethrow the same exception to the caller method. This behavior can seem a bit weird and counterintuitive: for this reason, there are some open bugs on the official bug tracker.

This method can be fixed easily, we just need to move the coroutineScope invocation inside the try/catch:

In this way the catch manages the coroutineScope exception and not the one thrown by await. The nested scope groups just the two calls that are logically connected: callServer and loadPrefs.

What about SupervisorJobs?

The example in this post uses a SupervisorJob to define the viewModelScope to simulate its real counterpart, defined inside the Android Support Library. The full example can be retrieved clicking on the + on the top border of the playground code.

Even using a SupervisorJob the exception causes an application crash, the reason can be found taking a look at the documentation:

a failure of a child job that was created using launch can be handled via CoroutineExceptionHandler in the context

The default handler on Android crashes the app on uncaught exceptions.

Another alternative is to use supervisorScope instead of coroutineScope in the loadData method (outside the try/catch):

The supervisorScope method is similar to coroutineScope but uses, under the hood, a SupervisorJob. This version of the method works, looking at the documentation we can see that:

a failure of a child does not cause this scope to fail and does not affect its other children

It works but an error on callServer does not cancel the loadPrefs invocation, so the total execution time is higher. The loadData method waits for the end of loadPrefs invocation even if the result value will be ignored.

Wrapping up

Coroutine code is readable and really easy to understand, however sometimes it’s not easy to find the best way to write it. Here there are some opinionated suggestions on how to use coroutines in an easy way:

  • always use launch on a top level scope to create a new coroutine
  • if you don’t need parallel executions just use withContext to change the execution thread
  • if you need to execute something in parallel use async on a nested scope created using coroutineScope. Try to keep the nested scope as small as possible and never add a try/catch inside a coroutineScope!

--

--

Android GDE || Android engineer @nytimes || author @androidavanzato || blogger @codingjam