I am trying to explore and implement coroutine cancellation / exception recovery mechanism in ViewModel. I discovered that the following code in my ViewModel doesn't catch the exception and crashes the app:
viewModelScope.launch {
try {
supervisorScope {
launch {
throw Exception()
}
}
} catch (e: Exception) {
println("Exception caught")
}
}
But if I replace supervisorScope
with coroutineScope
it gets caught. Shouldn't it get caught in both cases? Can anyone please explain why supervisorScope scope exception cancels its parent scope here?
I tried running following code in Intellij: case1 :
runBlocking {
supervisorScope {
launch {
throw Exception("Supervisor launch exception")
}
}
}
vs case 2:
runBlocking {
launch {
throw Exception("Launch Exception")
}
}
In first case the process finished with exit code 0 and in second case, finished with exit code 1. Why does it give different exit code when both propagates exception to parent?
The difference between exception handling in coroutineScope
and supervisorScope
is in children failure propagation.
In coroutineScope
all children coroutines delegate handling of their exceptions to their parent coroutine. When one of them encounters an exception other than CancellationException
, it cancels its parent with that exception. That's why try
with coroutineScope
works - coroutineScope
just rethrows the exception from its children coroutine.
In supervisorScope
child's failure does not propagate to the parent. Every child should handle its exceptions by itself via CoroutineExceptionHandler
. If it doesn't, the exception becomes uncaught and goes up until it finds a parent coroutine with a CoroutineExceptionHandler
installed. If no CoroutineExceptionHandler
was found, the exception will be handled by Thread.defaultUncaughtExceptionHandler
or, if it is not set, just printed to the error output stream.
First example:
supervisorScope
doesn't know about its child coroutine fail, so try...catch
won't work. To handle the exception, install a CoroutineExceptionHandler
on either viewModelScope.launch
or the inner launch
.
Case 1:
launch
in supervisorScope
doesn't delegate exception handling and doesn't handle the exception itself, the exception is uncaught. runBlocking
doesn't have a CoroutineExceptionHandler
, so Thread
's uncaught exception handling mechanism handles it.
Case 2:
launch
delegates exception handling to runBlocking
, because it is not supervised. CoroutineExceptionHandler
wouldn't help in this case, because it is not an uncaught exception. The exception is unhandled by runBlocking
, exit code is 1.