Basically I have a situation where I want a 'best-...
# coroutines
o
Basically I have a situation where I want a 'best-effort' behavior, but with nested scopes. Do I need to declare a scope with SupervisorJob at each scope creation? I assume yes because that seems more predictable (but more verbose), but I want to make sure.
d
Why not just handle the exceptions within the coroutine instead of letting them fly? At least then you don't worry about cancelling scopes unless something dire happens.
o
I am, but even caught exceptions will cancel the scope (hence the SupervisorJob), unless I misunderstood.
I don't want any of the nested scopes to be canceled, either. Documentation for
coroutineContext
says that it inherits the parent's context, but overrides the parent's `Job`; I want to make sure that that means that the
SupervisorJob
is overriden.
b
Correct, SupervisorJob will not cancel due to a child completing exceptionally
o
Right, and nested scopes?
d
Wait, so if you catch an exception at
C
, then
B
still gets cancelled?!
o
Copy code
This function is designed for a parallel decomposition of work. When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled (for a different behavior see supervisorScope).
From the docs for
coroutineScope {}
d
Yeah but if you catch the exception inside the coroutine, the coroutine doesn't 'fail'.
o
Are you certain? If you're calling a suspending library, and that library fails with an exception, the state machine will have seen the exception, right?
d
Hmm, it will have technically seen it yes but it doesn't necessarily count as a 'fail'.
I'm not 100% certain but I'm pretty sure.
b
You can catch exceptions. You're correct that the state machine recognizes the exception and will throw it, but we're not talking about a suspending function failing, we're talking about coroutines (which execute suspending functions) failing
Copy code
async {
    try {
        error("oops")
    } catch (err: Throwable) {
        //Exception caught, async coroutine does not complete exceptionally
    }
}
In your example,
coroutineScope { }
is launching a child coroutine of your
SupervisorJob
Also, it’s better to do
supervisorScope { }
instead of
with(CoroutineScope(coroutineContext + SupervisorJob())) { }
o
Yes, but what is its behavior? That was my original question; will it behave like a regular coroutineScope, propagating exceptions bidirectionally, but stopping at the boundary of the supervisor? That's the expected behavior, which I'd still like to confirm.
Also, the structure is more important than the specific example. If I use
supervisorScope {}
I can't use a specific dispatcher, while I can if I use
the CoroutineScope
factory function.
d
I think I remember someone saying the dispatcher thing was by design. You're not supposed to be using
supervisorScope
.
o
Thanks, that thread is useful.
b
I guess I’m not sure what you’re meaning by propagating exceptions “bidirectionally”. When a child coroutine completes exceptionally, it propagates that to the parent Job. It’s up to the parent job on whether that job completes exceptionally too. In the case of a supervisor job, it will not cancel itself or its children with the exception, making that exception isolated to the child job. That occurs for the entire structured hierarchy. So if you have a 3 levels of hierarchy, the first and third being regular jobs and the second being a supervisor. if the third job completes exceptionally, the second (supervisor) job is notified and it no-ops, making the exception not propagate along the hierarchy.
supervisorJob { }
suspends until all its child jobs have completed, ensuring that you do not leak running jobs. By doing
with(CoroutineScope(coroutineContext + SupervisorJob())) { ... }
, you are breaking the hierarchy with a job that has no parent. This is the reason
coroutineScope { }
exists and behaves the exact same way, but with a normal Job instead of SupervisorJob. If you want to modify the dispatcher, you should set the context using
withContext()
instead
Copy code
supervisorJob {
    withContext(<http://Dispatcher.IO|Dispatcher.IO>) {
        ...
    }
} // waits until all children jobs are completed

with(CoroutineScope(coroutineContext + <http://Dispatchers.IO|Dispatchers.IO> + SupervisorJob())) {
    ...
} // does not wait for children jobs to complete - also the jobs launched inside this block are not tracked to a parent job, breaking structured concurrency
o
Again, it was about the structure, please don't fixate on the details -- I actually was instancing and holding onto
SupervisorJob()
. In previous discussions creating a scope that way was actually the recommended solution.
In fact, it is even in the official documentation.
And while I appreciate giving suggestions on things I didn't directly ask about, the question on how a supervisor scope interacts with nested scope was unanswered until Dominic found/posted the thread above.
b
I guess I wasn't sure what your question actually was then. The thread that was linked goes over the same reasoning why you should use
supervisorScope { }
instead of a factory method that's blindly overwriting the job the context is tracked to. If my original comments weren't helpful, hopefully you received the answer you were looking for 👍
SupervisorJob will not cancel due to a child [coroutine] completing exceptionally
coroutineScope { }
is launching a child coroutine of your
SupervisorJob