
Managing exceptions in nested coroutine scopes
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 withtrue
as argumentloadPrefs
lasts half a second and fails when called withtrue
as argumentloadFallback
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 aCoroutineScope
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 usingcoroutineScope
. Try to keep the nested scope as small as possible and never add atry/catch
inside acoroutineScope
!