Kotlin coroutines is a great feature that allows you to write asynchronous code in synchronous style.
It’s absolutely perfect until you need to investigate problems in your code.
One of the common problems with coroutines is the shortened stack trace in exceptions thrown in coroutines. For example, this code prints out the stack trace below:
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
suspend fun fun1() {
delay(100)
throw Exception("exception at ${System.currentTimeMillis()}")
}
suspend fun fun2() {
fun1()
delay(100)
}
suspend fun fun3() {
fun2()
delay(100)
}
fun main() {
try {
runBlocking {
fun3()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
java.lang.Exception: exception at 1641336576296
at MainKt.fun1(main.kt:6)
at MainKt$fun1$1.invokeSuspend(main.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
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:21)
at MainKt.main(main.kt)
The stack trace doesn’t represent the true coroutine call stack when the exception was created: calls of functions fun3 and fun2 are absent. On complex systems, this problem can make debugging much more difficult.
The Kotlin team are known about the problem and has come up with a solution, but it solves just a part of the cases.
The reason why the stack trace is shortened is that when the coroutine wakes up, only the last method of its call stack is called.
Therefore, after studying the implementation of coroutines, I wrote my own solution based on bytecode generation and MethodHandle API. I called it Stacktrace-decoroutinator. My library replaces the coroutine awakening implementation. It generates classes at runtime with names that match the entire coroutine call stack. These classes don’t do anything except call each other in the coroutine call stack order. Thus, if the coroutine throws an exception, they mimic the real call stack of the coroutine during the creation of the exception stacktrace.
In the near future I am going to test the library in my production environment and I will be glad if someone else will join.