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 Exceptionephemient
02/03/2023, 2:14 AMSupervisorJob() at the top only handles direct children, so that's not relevant to thisephemient
02/03/2023, 2:19 AMcatch 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)ephemient
02/03/2023, 2:20 AMCoroutineExceptionHandler 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 AMursus
02/03/2023, 3:04 AMursus
02/03/2023, 3:05 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 asyncephemient
02/03/2023, 3:09 AMephemient
02/03/2023, 3:09 AMval 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 AMursus
02/03/2023, 3:13 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 normalephemient
02/03/2023, 3:15 AMthrow 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 AMephemient
02/03/2023, 3:19 AMursus
02/03/2023, 3:20 AMursus
02/03/2023, 3:21 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 scopesephemient
02/03/2023, 3:21 AMursus
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 AMephemient
02/03/2023, 3:27 AMursus
02/03/2023, 3:32 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)ephemient
02/03/2023, 3:39 AMval 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 AMPablichjenkov
02/03/2023, 4:21 AMgildor
02/03/2023, 6:10 AMgildor
02/03/2023, 6:12 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 AMgildor
02/06/2023, 10:15 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 processephemient
02/06/2023, 1:06 PMtry-catch will not catch everything, that's literally where we started this conversation from