Reputation: 133
I was playing around with coroutines and found some very strange behavior. I want to convert some asynchronous requests in my project using suspendCoroutine()
. Here's piece of code showing this problem.
In first case, when suspend function is being called in runBlocking
coroutine, exception from continuation goes to catch block, and then runBlocking
finishes successfully. But in second case, when creating new async
coroutine, exception goes through catch block and crashes the whole program.
package com.example.lib
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
object Test {
fun runSuccessfulCoroutine() {
runBlocking {
try {
Repository.fail()
} catch (ex: Throwable) {
println("Catching ex in runSuccessfulCoroutine(): $ex")
}
}
}
fun runFailingCoroutine() {
runBlocking {
try {
async { Repository.fail() }.await()
} catch (ex: Throwable) {
println("Catching ex in runFailingCoroutine(): $ex")
}
}
}
}
object Repository {
suspend fun fail(): Int = suspendCoroutine { cont ->
cont.resumeWithException(RuntimeException("Exception at ${Thread.currentThread().name}"))
}
}
fun main() {
Test.runSuccessfulCoroutine()
println()
Test.runFailingCoroutine()
println("We will never get here")
}
That's what is printed on console:
Catching ex in runSuccessfulCoroutine(): java.lang.RuntimeException: Exception at main
Catching ex in runFailingCoroutine(): java.lang.RuntimeException: Exception at main
Exception in thread "main" java.lang.RuntimeException: Exception at main
at com.example.lib.Repository.fail(MyClass.kt:32)
at com.example.lib.Test$runFailingCoroutine$1$1.invokeSuspend(MyClass.kt:22)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:236)
at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.lib.Test.runFailingCoroutine(MyClass.kt:20)
at com.example.lib.MyClassKt.main(MyClass.kt:41)
at com.example.lib.MyClassKt.main(MyClass.kt)
Process finished with exit code 1
Any ideas why this is happening - is it a bug, or am i using coroutines the wrong way?
Update:
Using coroutineScope { ... }
will mitigate problem in runFailingCoroutine()
fun runFailingCoroutine() = runBlocking {
try {
coroutineScope { async { fail() }.await() }
} catch (ex: Throwable) {
println("Catching ex in runFailingCoroutine(): $ex")
}
}
Upvotes: 13
Views: 17234
Reputation: 200256
I got struck by this behavior just yesterday, here's my analysis.
In a nutshell, this behavior is desired because async
does not have the same purpose as in other languages. In Kotlin you should use it sparingly, only when you have to decompose a task into several subtasks that run in parallel.
Whenever you just want to write
val result = async { work() }.await()
you should instead write
val result = withContext(Default) { work() }
and this will behave the expected way. Also, whenever you have the opportunity, you should move the withContext
call into the work()
function and make it a suspend fun
.
Upvotes: 7
Reputation: 6258
The behavior of your second example is correct, this is the work of structured concurrency.
Because the inner async
block throws an exception, this coroutine is cancelled. Due to structured concurrency the parent job is cancelled as well.
Look at this small example:
val result = coroutineScope {
async {
throw IllegalStateException()
}
10
}
This block will never return a value, even if we never request the async
result. The inner coroutine is cancelled and the outer scope is cancelled as well.
If you don't like this behavior you can use the supervisorScope
. In this case the inner coroutine can fail without failing the outer coroutine.
val result = supervisorScope {
async {
throw IllegalStateException()
}
10
}
In your first example you catch the exception inside of the coroutine block, because of this, the coroutine exits normally.
For discussion of this topic see:
Upvotes: 8