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")
}
}
A generic way to handle exception in kotlin is to use a try-catch block. Where we write our code which might throw an exception in the try block, and if there is any exception generated, then the exception is caught in the catch block.
In Kotlin, we use try-catch block for exception handling in the program. The try block encloses the code which is responsible for throwing an exception and the catch block is used for handling the exception. This block must be written within the main or other methods.
Utilities for Reactive Streams. If these adapters are used along with kotlinx-coroutines-reactor in the classpath, then Reactor's Context is properly propagated as coroutine context element ( ReactorContext ) and vice versa.
withContext is a scope function that allows us to create a new cancelable coroutine. If we pass a CoroutineContext arg, withContext merges the parent context and our arg to create a new CoroutineContext, then executes the coroutine within this merged context.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With