Library for recovering stack traces in exceptions thrown in Kotlin coroutines.
Supports JVM 1.8 or higher and Android API 14 or higher.
Coroutines are a significant Kotlin feature that allows you to write asynchronous code in a synchronous style.
This works perfectly until you need to debug problems in your code.
One of the common problems 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(10)
throw Exception("exception at ${System.currentTimeMillis()}")
}
suspend fun fun2() {
fun1()
delay(10)
}
suspend fun fun3() {
fun2()
delay(10)
}
fun main() {
try {
runBlocking {
fun3()
}
} catch (e: Exception) {
e.printStackTrace()
}
}java.lang.Exception: exception at 1641842199891
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 is missing calls to fun3 and fun2, so it does not represent the true coroutine call stack.
In complex systems, even more calls may be missing. This can make debugging much more difficult.
Some examples of suffering from this problem:
- arrow-kt/arrow#2647
- https://stackoverflow.com/questions/54349418/how-to-recover-the-coroutines-true-call-trace
- https://stackoverflow.com/questions/69226016/how-to-get-full-exception-stacktrace-when-using-await-on-completablefuture
The Kotlin team is aware of the problem and has come up with a solution, but it only addresses some cases. For example, the exception from the example above still lacks some calls.
Decoroutinator replaces the coroutine resumption implementation.
It generates methods at runtime with names that match the entire coroutine call stack.
These methods do nothing except call each other sequentially in coroutine call stack order, creating real JVM stack frames.
Thus, if the coroutine throws an exception, the stack trace reflects the full coroutine call chain.
Check out the Decoroutinator playground.
There are three ways to enable Decoroutinator on the JVM:
- If you build your project with Gradle, apply the Gradle plugin with id
dev.reformator.stacktracedecoroutinator. - Add
-javaagent:/path/to/stacktrace-decoroutinator-jvm-agent-2.6.4.jarto your JVM startup arguments. The corresponding dependency isdev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm-agent:2.6.4. - Add the dependency
dev.reformator.stacktracedecoroutinator:stacktrace-decoroutinator-jvm:2.6.4and callDecoroutinatorJvmApi.install().
The first option generates auxiliary methods at build time; the other two use the Java instrumentation API at runtime.
Usage example:
package dev.reformator.stacktracedecoroutinator.jvmtests
import dev.reformator.stacktracedecoroutinator.jvm.DecoroutinatorJvmApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
object Test {
suspend fun rec(depth: Int) {
if (depth == 0) {
yield()
throw Exception("exception at ${System.currentTimeMillis()}")
}
rec(depth - 1)
}
}
fun main() {
DecoroutinatorJvmApi.install() // enable stacktrace-decoroutinator runtime
try {
runBlocking {
Test.rec(10)
}
} catch (e: Exception) {
e.printStackTrace() // print full stack trace with 10 recursive calls
}
}prints out:
java.lang.Exception: exception at 1764729227496
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:11)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test$rec$1.invokeSuspend(example.kt)
at kotlin.coroutines.jvm.internal.DecoroutinatorBaseContinuationAccessorImpl.invokeSuspend(base-continuation-accessor.kt:15)
at dev.reformator.stacktracedecoroutinator.common.internal.DecoroutinatorSpecImpl.resumeNext(utils-common.kt:247)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.Test.rec(example.kt:13)
at dev.reformator.stacktracedecoroutinator.jvmtests.ExampleKt$main$1.invokeSuspend(example.kt:21)
at dev.reformator.stacktracedecoroutinator.mhinvoker.internal.RegularMethodHandleInvoker.callSpecMethod(mh-invoker.kt:20)
at dev.reformator.stacktracedecoroutinator.common.internal.AwakenerKt.callSpecMethods(awakener.kt:225)
at dev.reformator.stacktracedecoroutinator.common.internal.AwakenerKt.awake(awakener.kt:36)
at dev.reformator.stacktracedecoroutinator.common.internal.Provider.awakeBaseContinuation(provider-impl.kt:38)
at dev.reformator.stacktracedecoroutinator.provider.internal.DecoroutinatorProviderInternalApiKt.awakeBaseContinuation(provider-api-internal.kt:20)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at dev.reformator.stacktracedecoroutinator.jvmtests.ExampleKt.main(example.kt:20)
at dev.reformator.stacktracedecoroutinator.jvmtests.ExampleKt.main(example.kt)
For Android, the only supported option is to apply the Gradle plugin to your application project:
plugins {
id("dev.reformator.stacktracedecoroutinator") version "2.6.4"
}Note that Decoroutinator uses the MethodHandle API, which requires Android API 26 (Android 8) or higher. Stack trace recovery does not work on older Android versions.
Decoroutinator also allows embedding DebugProbes into your Android application. DebugProbes is a mechanism for dumping coroutine state and stack traces at runtime, useful for debugging.
stacktraceDecoroutinator {
embedDebugProbesForAndroid = true
// or the following if you want to embed DebugProbes only for tests:
// embedDebugProbesForAndroidTest = true
}If you want to use Decoroutinator for tests only, the recommended approach is to place your tests in a separate Gradle subproject and apply the Decoroutinator Gradle plugin there.
If you cannot separate your tests, you can still restrict it by adding the following to your build.gradle.kts:
stacktraceDecoroutinator {
androidTestsOnly = true
}Add the following ProGuard configuration to your build.gradle.kts:
android {
buildTypes {
release {
proguardFiles(decoroutinatorAndroidProGuardRules())
}
}
}Kotest 6.0 offers Decoroutinator support out of the box. See the documentation on how to integrate here.
There is a bug in the Shadow Gradle plugin that may cause build issues when both the Decoroutinator Gradle plugin and Shadow are applied. Several workarounds are available. See #46 for details.
Using Decoroutinator as a Java agent alongside Jacoco may result in lost code coverage. This is a known Jacoco limitation. To avoid losing coverage, ensure the Jacoco agent is listed before the Decoroutinator agent in your JVM arguments. See #24 for details.
Robolectric places some Decoroutinator classes in separate class loaders by default, which causes an exception during test execution. To fix this, add the following to your build.gradle.kts:
android {
testOptions {
unitTests.all {
it.systemProperty(
"org.robolectric.packagesToNotAcquire",
"dev.reformator.stacktracedecoroutinator."
)
}
}
}To check whether Decoroutinator has been successfully installed at runtime, call:
DecoroutinatorCommonApi.getStatus { it() }Feel free to ask any questions at Discussions.