Reputation: 1902
I want to catch an exception that is thrown from async coroutines. The following code demonstrates a problem:
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
try {
println(failedConcurrentSum())
} catch (e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum() = coroutineScope {
try {
val one = async {
try {
delay(1000L)
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
} catch (e: ArithmeticException) {
println("Using a default value...")
0
}
}
This prints:
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
The try-catch
inside the failedConcurrentSum
doesn't handle the exception thrown by val two
.
I can convince myself that this is due to the "structured concurrency".
However, this doesn't explain why wrapping the async
's inside a coroutineScope
catches the exception:
suspend fun failedConcurrentSum() = coroutineScope {
try {
val one = coroutineScope {
async {
try {
delay(1000L)
42
} finally {
println("First child was cancelled")
}
}
}
val two = coroutineScope {
async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
}
one.await() + two.await()
} catch (e: ArithmeticException) {
println("Using a default value...")
0
}
}
This prints:
First child was cancelled
Second child throws an exception
Using a default value...
0
Why does the latter catches an exception whereas the first doesn't?
Upvotes: 6
Views: 2520
Reputation: 200158
coroutineScope
is just a function, it sets up its scope internally and from the outside it always completes like a regular function, not messing with any outer scopes. This is because it doesn't leak any concurrent coroutines started within its scope. You can always reliably catch and handle exceptions thrown by coroutineScope
.
async
, on the other hand, completes immediately after launching the coroutine, therefore you have two concurrent coroutines: one running the async
code and another calling the corresponding await
. Since the async
one is also the child of the one calling await
, its failure cancels the parent before the parent's await
call completes.
The try-catch inside the
failedConcurrentSum
doesn't handle the exception thrown byval two
.
It actually does, if it gets the chance. But since the try-catch block is in a coroutine that runs concurrently to the one completing val two
's Deferred
, it just doesn't get the chance to do so before being cancelled due to the failure of the child coroutine.
Upvotes: 3
Reputation: 9682
coroutineScope
uses Job
By default, a failure of any of the job’s children leads to an immediate failure of its parent and cancellation of the rest of its children. Job
You can use supervisorScope
instead of coroutineScope
A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children. SupervisorJob
but you have to wait for the completion of the first async
block.
Use coroutineScope
inside try catch
to return a default value immediately when an exception occurs
suspend fun failedConcurrentSum() = try {
coroutineScope {
val one = async {
try {
delay(1000L)
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
} catch (e: ArithmeticException) {
println("Using a default value...")
0
}
Upvotes: 2