Kotlin coroutines stack trace problem

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.

1 Like

I have only a limited understanding of coroutines internals, but I believe a continuation keeps the reference to the continuation of the calling function. If this is the case, wouldn’t it be possible to iterate over the continuations chain in order to recreate a call stack? I’m just thinking, probably saying something silly :wink:

1 Like

Yeah, you’re right. It’s exactly how Stacktrace-decoroutinator and Kotlin stdlib restore the coroutine call stack.

1 Like

So if there is no problem in re-creating the true call stack then what exactly is the problem? Why do we have to create fake classes, why can’t we just use setStackTrace() to fill whatever we need?

I don’t try to argue or suggest that your tool is not needed. I’m honestly curious, because it sounds interesting :slight_smile:

2 Likes

Kotlin stdlib does almost as you say. But it doesn’t always work, because it’s not always possible to handle exception creation. For instance, stdlib cannot handle the example from the post.

Stacktrace-decoroutinator bypasses having to handle exception creation because it just creates real call stack at runtime.