Hi everybody!! Acording to the documentation, `asy...
# announcements
j
Hi everybody!! Acording to the documentation,
async()
exposes users to exceptions:
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce).
But then, I'd expect wrarping the
await()
call in
try... catch
would just allow me to catch the exception produced in the
async
block. Instead, it seems the
async()
block's exception is handled by the supervisor. That gives me an odd behavior in this example:
Copy code
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
    try {
        val res = async {
          // suspend fun that ends up throwing ArithmeticException
          failedConcurrentSum()
        }
        res.await()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}
The odd part is that the exception is caught by
catch
but, at the same time, it is thrown by runBloking. See an executable snippet here: https://pl.kotl.in/sWjfVuEDr. Why is that so? What am I missing here??
b
because you launched a coroutine (via async) on [this] scope. When error happens inside of this coroutine, it cancels runBlocking's scope exceptionally. Typically you have to bound your async operations to
coroutineScope
builder:
Copy code
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> { // Scope1
    try {
       coroutineScope { // Scope2
             val res = async {
             // suspend fun that ends up throwing ArithmeticException
             failedConcurrentSum()
             }
             res.await()
        }
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}
In this case, when error happens in async, it only cancels
Scope2
. Then exception propagated to scope1, but you catch it
j
Thank you! I see. But still, my question holds. Isn't it really odd that I get the exception caught and, at the same time, I have it thrown by
runBlocking
? I'd expect async to automatically create such a coroutineScope for me, so that if I catch whatever it throws upon await, I don't get it kind of "duplicated".
I mean, something like this `safeAsync`: https://pl.kotl.in/icYB5Ayri
b
yes, it kinda odd. You caught it, but it doesn't reset 'cancellation' of scope.
I'd expect async to automatically create such a coroutineScope for me, so that if I catch whatever it throws upon await, I don't get it kind of "duplicated".
it's done this way to handle this:
Copy code
coroutineScope { 
   val a = async { longOperation() }
   val b = async { shortButFailedOperation() }
   doSmth(a.await(), b.await())
}
If the error was propagated only when you call
.await()
the code above would wait for completion of
a
despite it doesn't make sense. Currently once
b
failed,
coroutineScope
failed as well, so it cancels
a
.
your safeAsync function is actually became
sync
, because coroutineScope waits for all children
so if you will call
Copy code
val a = safeAsync { doSmth() }
val b = safeAsync { doSmthElse() }
doSmthElse
will be executed strictly after completion of
doSmth
.
j
Oh, I see... 🤔
I see why my toy workaround does not work. Thanks!
But I don't understand this:
If the error was propagated only when you call 
.await()
 the code above would wait for completion of 
a
 despite it doesn't make sense. Currently once 
b
  failed, 
coroutineScope
 failed as well, so it cancels 
a
.
In that example, let's say I handle an error in the first async at
await
time. I understand that is not how this is designed to work, but it would be much more clear to me if that avoided b to be cancelled:
Copy code
coroutineScope { 
   val a = async { longOperation() }
   val b = async { shortButFailedOperation() }
   val actualA = try { a.await() } catch (_: Exception) { 23 }
   doSmth(actualA, b.await())
}
Note this (handling errors at
a.await()
)is something I may need to do in `doSmth`instead. And
doSmth
doesn't contain the actual code used to calculate the
Deferred
it receives as an argument.
I would find it more logical for whatever is launched with
async
to not cancel anything until someone `await`s on that and an exception is thrown.
b
yes, it doesn't check how you use await, and whether it makes sense to keep scope active or no.
note, that scope's cancellation behavior depends on its type. If you change
coroutineScope
to
supervisorScope
you will see desired behavior
j
My example above would seem more natural to me if it worked as I expected (of course, that's the reason I expected it so 😛 )
b
in
supervisorScope
child cancellation doesn't affect other children. So in supervisor scope exception will be propagated on
await()
call
🤔 1
j
b
would not cancel anything on failure, because it returned a Deferred that would carry its result
and then, the parent scope would be cancelled if it awaited on b and didn't handle the exception, thus cancelling any children that may be still computing anything