Hello. Could you explain how exception in coroutin...
# coroutines
m
Hello. Could you explain how exception in coroutines work? I have this demo code:
Copy code
import 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):
Copy code
Calculation error

Process finished with exit code 0
But if I call
calculateAverageAsync
in
runBlocking's scope
:
Copy code
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:
Copy code
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 code
s
Failure of a job created with
launch
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.
m
Unfortunately, in real task I have few parallel async jobs (network requests to 3 different services). And after completion I need to merge their results. If one of the requests fails I have to cancel others, but recover from the error and continue processing. So if I have
coroutineScope -> 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)
s
Did you try using
coroutineScope
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.
m
I checked it by the code:
Copy code
import 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:
Copy code
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!
I think it should be in coroutines documentation
s
The distinction is because
coroutineScope
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
.
The rule to follow is: •
suspend
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.
m
Thank you for explanation and link. I will reread the documentation
s
Also super helpful is https://elizarov.medium.com/explicit-concurrency-67a8e8fd9b25. This article is the reference for my comment about suspend functions vs CoroutineScope extensions.
👍 1