```protected val scope = CoroutineScope(Supervisor...
# coroutines
u
Copy code
protected 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
Copy code
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
e
SupervisorJob()
at the top only handles direct children, so that's not relevant to this
the
catch
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)
if there's a
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 exception
p
Something like, the supervisorScope doesn't report the exception through the continuation, which is what try expects. It levels the exception up to it's parent scope.
u
I see, so it doesnt pass the exception to the next suspend function, because it would have to then resume multiple times which would break the contract
but rather "side effects" the exception, but since android has no default handler, it crashes
soo..should I just go back to
coroutineScope
and try catch inside the `launch`es?
e
as I mentioned.
async
will "handle" the exception, so
Copy code
supervisorScope {
    async { error("a") }
    async { error("b") }
}
ignores the errors (of course you can also add completion handlers if you want)
u
hm but async doesnt propagate the error right? therefore I might as well use coroutineScope+async?
e
no,
coroutineScope
will be cancelled when an exception happens, even inside an
async
it propagates normally in that case
if you want to collect all the errors while proceeding, then
Copy code
val thrown = mutableListOf<Throwable>()
withContext(CoroutineExceptionHandler { _, t -> thrown.add(t) }) {
    supervisorScope {
        launch { error("a") }
        launch { error("b") }
    }
}
will handle them without additional work per child
u
Im a bit confused, so if async propagates and therfore coroutineScope get canceled, but if it were to propagate then supervisorScope would crash,
so it is somehow special, that it doesnt make supervisorScope crash?
e
when there is a
throw
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 normal
when there is a
throw
inside
async
inside
supervisorScope
,
supervisorScope
doesn't do anything with it, so it simply completes the
Deferred
with the exception
u
so there is special handling of Deffered, I see (I wasnt getting why would async work over launch. I though supervisor just swallows exceptions, no matter what kind of coroutine produced it
e
there is nothing particularly special about it
normally, any exception cancels the whole job tree
u
okay so because async handled exceptions differntly in general? and If I were to have exception handler setup, it would behave the same way?
so Im basically failing just because not having "logger" setup (with the launches)
e
but because
supervisorScope
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 scopes
didn't I give an example of having a handler just above?
u
yes you did, I'm trying to understand so there wasn't nothing particularly wrong with my mental model, just a "logger" was uninitialized and thats the whole issue? (i.e. a no-op non null exc. handler would make it all work as expected)
e
inside a normal
coroutineScope
, the
CoroutineExceptionHandler
isn't useful because it'll cancel the tree first
u
okay so the handler is not just a simple interface where to "report" exceptions to? (the way thread uncaught handler is? .. alteast I think it is 😄 )
e
sure, it's basically a scoped uncaught exception handler
where normal cancellation counts as being handled, so that doesn't go through it
u
I see so thr issue is the children get canceled anyways with coroutineScope, not inside the handler or whatever was I thinking
Okay I understand now, thank you!
e
just to hopefully clear up some things I might have said ambiguously,
Copy code
val 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)
whereas doing the same with
Copy code
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)
u
Yes, I was basically looking for what I needed to add to make supervisorScope+launch work (besides the async alternative)
p
As a rule of thumb, I try to avoid my code logic depending on the exception propagation logic of the coroutine cluster. I rather try/catch or runCatching in the suspicious scope and convert the exception into an error domain value.
Or use flow where the catching is trivial
g
Totally with @Pablichjenkov here. We have 0 cases of exception handler usage in the project and never regret it. Handle exceptions in the code which runs coroutine, use simple try/catch everywhere
Essentially for you example, I would remove supervisorScope from sync and handle exception on level of each launch or just wrap coroutineScope of sync() with try/catch
e
I don't completely agree. yes, often you don't need an exception handler. but if you're creating local
GlobalScope
substitute,
SupervisorJob() + CoroutineExceptionHandler
is what you need
g
Why would you need an exception handler for GlobalScope?
e
not for
GlobalScope
, but for your own local
GlobalScope
substitute (e.g. something like
viewModelScope
on Android) it can be useful
g
Yes, I got it, why would you need it for viewModel? If it used by VM, why not handle all errors there explicitly instead of relying on error handler?
e
because sometimes you can't. suppose you're writing a server framework, and you want to have a coroutine scope per child, but unhandled exceptions from one request should not crash the whole process. then you need an exception handler. scoped
try
-
catch
doesn't cover everything, for reasons discussed above.
g
Yep, I totally see some cases like server framework, this why I asked about "view model", which, same as example from original message is UI related But even for server framework, I would probably instead use something like this: mySercerScope.launch { val endpoint = resolveEndpoint() val responce = try { coroutineScope { endpoint.process(request) } } catch(e) { processEndpointException(endpoint, e) } } So no custom error handlers which also can escape it It doesn't mean that there are no use cases, just that I believe those cases are much more rare
I'm not against the whole idea of error handlers, there are for sure some use cases, but I believe that all cases which I see in this channel are better to handle different ways and not rely on error handler at all
e
I brought up
viewModelScope
as possibly the best-known example of a scope that is "global" to a certain lifetime that is shorter than the whole process
but did you read up-thread? that
try
-
catch
will not catch everything, that's literally where we started this conversation from