hi all, I have something weird happening in my and...
# coroutines
a
hi all, I have something weird happening in my android app, but i dont think it is android related. essentially, i have this util function
Copy code
suspend fun joinAll(vararg blocks: suspend () -> Unit) = supervisorScope {
    blocks.map {
        launch { it() }
    }.joinAll()
}
its for launching multiple jobs in parallel inside any suspend fun, and waiting until theyre completed/failed before moving forward. Something strange that's happening is that jobs called this way that crash don't seem to get caught properly, ie
Copy code
try {
    joinAll(
        { throw NoSuchElementException }
    )
    println("continue as normal") // this doesnt run
} catch(e: NoSuchElementException) {
   // this doesnt run, app crashes instead
}
doesnt the supervisorScope imply that the child being launched should fail gracefully, and the println statement should run? and given that it crashes instead, why isnt the exception being caught?
changing supervisorScope to coroutineScope in my joinAll function fixes the issue by the way, but id like to maintain my current behaviour ideally
e
if an exception occurs inside a child of a regular coroutineScope, that results in it bubbling up through the usual scopes (and cancelling other jobs along the way)
a
ok, that makes sense. essentially, i should not be launching suspending code with my joinAll function that doesnt handle its own exceptions
e
if an exception occurs inside a child of a supervisorScope, it doesn't cancel other jobs - which means it isn't handled through the usual mechanisms
you could also use
async
instead of
launch
a
how would it differ if an async block threw inside supervisor scope?
s
Also, I dont think you'll need to call joinAll(), since a call to supervisor/coroutineScope already only resumes after all its child coroutines have completed/finished.
e
if
async
throws, the exception gets captured in that
Deferred
instead of going to the scope's CoroutineExceptionHandler
note that
awaitAll()
might not have the behavior you want there though, since it'll cancel remaining deferred if one of them is failed
a
ah i see. well i don't love using async without using the result cause it gives you a warning, i think i'll stick to launch and handle the exceptions explicitly
thank u for ur help, lots to learn with coroutines blob sweat smile oh, i guess i wouldnt get the warning in this case, because i am using the result of the async (by returning it in the map block)
e
if you want to allow for multiple of the async's to fail, you need to decide what to do with potential multiple failures
for example,
Copy code
suspend fun joinAll(vararg blocks: suspend () -> Unit) {
    val exceptions = supervisorScope {
        blocks.map {
            async { it() }
        }
    }.mapNotNull { it.getCompletionExceptionOrNull() }
    when (exceptions.size) {
        0 -> return
        1 -> throw exceptions.first()
        else -> throw RuntimeException("multiple exceptions").apply {
            for (exception in exceptions) addSuppressed(exception)
        }
    }
}
s
^^^ In that case, i'd go a bit of a functional route, like returning (either) a result or a non-empty (Nel) of failure-instances/codes.
(not using exceptions; and keep exceptions for only truely fatal situations, warranting a crash)
If, of course, you dont' want one cancelled/failed child to cause its siblings to get cancelled as well.