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
import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApiimport kotlin.coroutines.debug.StackTraceRecoverableimport 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.
flowchart LR A["async: throw ApiException"] --> B["Copy + coroutine frames"] B --> C["await: receive 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:
-Dkotlinx.coroutines.debug=on-Dkotlinx.coroutines.stacktrace.recovery=trueDebug 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.