ursus
02/03/2023, 1:10 AMprotected val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun pullToRefresh() {
scope.launch {
try {
sync()
} catch(ex: Exception) {
// Never called <------------------
}
}
}
suspend fun sync() {
supervisorScope { <-----------
Log.d("Default", "a")
launch {
delay(200)
if (true) error("") <---------------
Log.d("Default", "b")
}
launch {
delay(300)
Log.d("Default", "c")
}
}
}
Why does supervisorScope
crash the app on android?
I want to run two syncs in parallel inside sync
and I don't want ones exception to cancale the other, so supervisorScoped
seemed what I want
but it crashes the app with
2023-02-03 02:11:55.860 foo.bar E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.IllegalStateException:
at foo.bar.DashboardViewModel$pullToRefresh$1$1$1.invokeSuspend(DashboardViewModel.kt:323)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@ebf2ef7, Dispatchers.Main.immediate]
Can't even catch it as if it were a Error
not a Exception
ephemient
02/03/2023, 2:14 AMSupervisorJob()
at the top only handles direct children, so that's not relevant to thiscatch
can only run after sync
completes, so it would either have to stop as soon as error()
happens (which happens in the non-supervisorScope
case), or exceptions have to go unhandled by this structure (which is what happens here: when the unhandled exception reaches the top level, you get the crash)CoroutineExceptionHandler
in context somewhere, it can handle the exceptions asynchronously, or if you use async
instead of launch
inside of supervisorScope
, that will also capture the exceptionPablichjenkov
02/03/2023, 2:50 AMursus
02/03/2023, 3:02 AMcoroutineScope
and try catch inside the `launch`es?ephemient
02/03/2023, 3:06 AMasync
will "handle" the exception, so
supervisorScope {
async { error("a") }
async { error("b") }
}
ignores the errors (of course you can also add completion handlers if you want)ursus
02/03/2023, 3:08 AMephemient
02/03/2023, 3:09 AMcoroutineScope
will be cancelled when an exception happens, even inside an async
val thrown = mutableListOf<Throwable>()
withContext(CoroutineExceptionHandler { _, t -> thrown.add(t) }) {
supervisorScope {
launch { error("a") }
launch { error("b") }
}
}
will handle them without additional work per childursus
02/03/2023, 3:12 AMephemient
02/03/2023, 3:14 AMthrow
inside async
inside a coroutineScope
(or almost any other scope for that matter), it cancels the parent (which cancels other children) and is propagated as normalthrow
inside async
inside supervisorScope
, supervisorScope
doesn't do anything with it, so it simply completes the Deferred
with the exceptionursus
02/03/2023, 3:18 AMephemient
02/03/2023, 3:19 AMursus
02/03/2023, 3:20 AMephemient
02/03/2023, 3:21 AMsupervisorScope
ignores failures of its children, async
gets to capture the exception without being cancelled. launch
doesn't have any way to capture the exception, and it can't be returned via the usual means because that would require supervisorScope
to handle it, so it gets passed up the parent scopesursus
02/03/2023, 3:22 AMephemient
02/03/2023, 3:23 AMcoroutineScope
, the CoroutineExceptionHandler
isn't useful because it'll cancel the tree firstursus
02/03/2023, 3:24 AMephemient
02/03/2023, 3:27 AMursus
02/03/2023, 3:32 AMephemient
02/03/2023, 3:38 AMval scope = CoroutineScope(CoroutineExceptionHandler { _, ex -> println("caught $ex") })
scope.launch { error("!!") }
scope.launch { delay(100); println("??") }
in this example, !!
gets printed by the handler because there's no parent, but ??
doesn't get printed because the `scope`'s own Job
becomes cancelled (if you don't specify a Job
when creating a scope, it'll add one itself)val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, ex -> println("caught $ex") })
will result in !!
printed by the handler and ??
printed normally, because the SupervisorJob
doesn't cancel when its children fail (same as the supervisorScope
case)ursus
02/03/2023, 3:42 AMPablichjenkov
02/03/2023, 4:19 AMgildor
02/03/2023, 6:10 AMephemient
02/03/2023, 7:13 AMGlobalScope
substitute, SupervisorJob() + CoroutineExceptionHandler
is what you needgildor
02/06/2023, 1:17 AMephemient
02/06/2023, 3:01 AMGlobalScope
, but for your own local GlobalScope
substitute (e.g. something like viewModelScope
on Android) it can be usefulgildor
02/06/2023, 4:42 AMephemient
02/06/2023, 5:40 AMtry
-catch
doesn't cover everything, for reasons discussed above.gildor
02/06/2023, 10:14 AMephemient
02/06/2023, 1:05 PMviewModelScope
as possibly the best-known example of a scope that is "global" to a certain lifetime that is shorter than the whole processtry
-catch
will not catch everything, that's literally where we started this conversation from