Skip to content
•
3 min read

Kotlin Coroutines Debugging Recipes: StackTraceRecoverable

Did you know about StackTraceRecoverable?

An exception thrown inside async tells you where it failed. The harder question is where another coroutine received that failure through await().

Kotlin 2.4.20 adds the experimental StackTraceRecoverable interface to the standard library. It lets custom exceptions define how they are copied for coroutine stack trace recovery, without depending on kotlinx.coroutines in the exception class itself. Kotlin release notes.

This matters when an exception carries extra state, such as an API error code. Override copyForStackTraceRecovery() to preserve that state and keep the original exception as the copy’s cause. The recovery machinery supplies the additional coroutine frames. API contract.

Keep the error code

StackTraceRecovery.kt
import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi
import kotlin.coroutines.debug.StackTraceRecoverable
import kotlinx.coroutines.*
@OptIn(ExperimentalStdlibCoroutineSupportApi::class)
class ApiException private constructor(
val errorCode: Int,
cause: Throwable?
) : Exception("API error: $errorCode", cause),
StackTraceRecoverable<ApiException> {
constructor(errorCode: Int) : this(errorCode, null)
override fun copyForStackTraceRecovery(): ApiException =
ApiException(errorCode, this)
}
fun main() = runBlocking {
supervisorScope {
// The coroutine that produces the exception.
val request = async<Int> {
delay(100)
throw ApiException(errorCode = 503)
}
try {
// The exception is rethrown here to the consumer.
request.await()
} catch (e: ApiException) {
println(e.errorCode) // 503
e.printStackTrace()
}
}
}

supervisorScope keeps the failed async child from cancelling its parent before the failure can be handled at await(). The copy retains errorCode = 503; this links it to the original exception.

With stack trace recovery enabled, the receiving coroutine gets a recovered copy.

Enable recovery on the JVM

Use Kotlin 2.4.20 or later with a compatible kotlinx-coroutines-core dependency. For this JVM example, add these VM options to the run configuration:

VM options
-Dkotlinx.coroutines.debug=on
-Dkotlinx.coroutines.stacktrace.recovery=true

Debug mode enables recovery by default; the second option makes the intent explicit. The interface is available in common code, but kotlinx.coroutines uses it for recovery only on the JVM. This is a JVM demo, not code to run on Android’s main thread. Coroutine debugging guide.

Printing 503 alone does not prove recovery ran—the original exception carries that value too. Inspect the recovered stack trace and its cause. Return null from copyForStackTraceRecovery() if copying should be disabled for that exception type.

Share this postLinkedIn ↗X ↗

Reactions & comments

Have something to add? Sign in with GitHub to react or join the discussion.

View discussions on GitHub ↗

Get new posts by email

Android, Kotlin, and Compose notes. Straight to your inbox.

Email address

Form not showing? Subscribe here ↗

Confirm your email to subscribe. Unsubscribe anytime. Powered by beehiiv.