Mikhail Buzuverov
02/17/2022, 4:22 AMimport kotlinx.coroutines.*
fun main() = runBlocking {
val average = GlobalScope.calculateAverageAsync(emptyList())
try {
println(average.await())
} catch (ex: ArithmeticException) {
println("Calculation error")
}
}
fun CoroutineScope.calculateAverageAsync(list: List<Int>): Deferred<Int> = async {
list.sum() / list.size
}
And it prints (as expected):
Calculation error
Process finished with exit code 0
But if I call calculateAverageAsync
in runBlocking's scope
:
import kotlinx.coroutines.*
fun main() = runBlocking {
val average = this.calculateAverageAsync(emptyList())
try {
println(average.await())
} catch (ex: ArithmeticException) {
println("Calculation error")
}
}
fun CoroutineScope.calculateAverageAsync(list: List<Int>): Deferred<Int> = async {
list.sum() / list.size
}
I got another result:
Calculation error
Exception in thread "main" java.lang.ArithmeticException: / by zero
at MainKt$calculateAverageAsync$1.invokeSuspend(main.kt:14)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(main.kt:3)
at MainKt.main(main.kt)
Process finished with exit code 1
As I can see the exception handler worked and printed "Calculation error". So exception was caught. But as different from previous case runBlocking
was cancelled and main thread got the same ArithmeticException
.
I don't understand how to handle exception in case of structured concurrency. Nothing I tried works - exception cancels jobs and reaches runBlocking
's outer codeSam
02/17/2022, 8:13 AMlaunch
or async
will always notify the parent job of the failure. That's a rule of structured concurrency and can't be changed. But what you can do is change the way the parent job responds to the failure of a child. My favourite way is to wrap the async call in a supervisorScope
, which allows its children to fail without failing itself.Mikhail Buzuverov
02/17/2022, 8:31 AMcoroutineScope -> 3 async -> 3 await -> merge
parent job (runBlocking
) will fail without possibility to recover after exception.
If I have supervisorScope -> 3 async -> 3 await -> merge
parent job (runBlocking
) will continue to work, but async will not be canceled automatically (I have to cancel them in catch block, that's error-prone place)Sam
02/17/2022, 8:39 AMcoroutineScope
and then wrapping that in a try/catch? I would expect that to work. The coroutineScope isn't the same as launch/async, it isn't part of the job hierarchy in the same way, so it will just throw exceptions instead of propagating them to any parent job.Mikhail Buzuverov
02/17/2022, 8:44 AMimport kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
val average = this.calculateAverageAsync(emptyList(), 100)
val average2 = this.calculateAverageAsync(listOf(100, 200), 500)
println(average.await() + average2.await())
}
} catch (ex: ArithmeticException) {
println("Calculation error")
}
}
fun CoroutineScope.calculateAverageAsync(list: List<Int>, delay: Long): Deferred<Int> = async {
try {
delay(delay)
list.sum() / list.size
} catch (ex: CancellationException) {
println("Cancelation ${ex.stackTraceToString()}")
throw ex
}
}
And it works! Output:
Cancelation kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=ScopeCoroutine{Cancelling}@5f2050f6
Caused by: java.lang.ArithmeticException: / by zero
<stack omitted>
Calculation error
Process finished with exit code 0
Thank you very much! It is really helpful!Mikhail Buzuverov
02/17/2022, 8:44 AMSam
02/17/2022, 8:49 AMcoroutineScope
isn't a background job. From the outside looking in, it's just a suspending function that returns a result or throws an error. Because it suspends until everything is complete, there's no need for structured concurrency to handle background errors. It's only inside the coroutineScope
that things start to run asynchronously and structured concurrency comes into play. You can see this because coroutineScope
itself is not an extension function on CoroutineScope
.Sam
02/17/2022, 8:51 AMsuspend
functions run to completion and then return a result or throw an error. They don't launch jobs that continue to run after the function returns, and they don't explicitly participate in structured concurrency.
• Extension functions on CoroutineScope
can launch background work and then return immediately, while the background jobs are still running. These functions participate in structured concurrency, to keep track of the background jobs and any errors they might throw.Sam
02/17/2022, 8:54 AMMikhail Buzuverov
02/17/2022, 8:57 AMSam
02/17/2022, 9:19 AM