You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin协程调用CompletableFuture.await()时获取完整异常栈

问题背景

使用kotlinx.coroutines.future.FutureKt#await等待CompletableFuture异步任务执行结果时,若异步任务内部抛出异常,上层捕获到的异常仅包含异步线程侧的调用栈,缺失协程侧的调用链路信息。

示例复现代码

fun main() {
    try {
        myFun1Blocking()
    } catch (e: Throwable) {
        e.printStackTrace(System.out)
    }
}

fun myFun1Blocking() {
    runBlocking {
        myFun2Suspend()
    }
}

suspend fun myFun2Suspend() {
    runAsync().await()
}

fun runAsync(): CompletableFuture<Void> {
    return CompletableFuture.runAsync {
        Thread.sleep(2000)
        throw Exception()
    }
}

原始异常输出(缺协程栈)

java.lang.Exception
    at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:34)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1728)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
现有方案缺陷
  • 方案1:捕获异常后重新包装抛出,仅能补充部分栈信息,仍然缺失上层挂起函数myFun2Suspend的调用记录。
  • 方案2:提前生成当前栈异常、捕获到异步异常时先打印预生成的栈再抛出原异常,虽然能拿到完整调用栈,但需要额外传入打印流,且会产生两次异常输出,实现冗余。
优化解决方案

方案A:开启协程调试模式(无代码侵入)

直接在JVM启动参数中添加配置:-Dkotlinx.coroutines.debug=on,开启后Kotlin协程会自动恢复完整的挂起调用栈,异常打印时会自动包含协程侧的全部调用链路,不需要修改任何业务代码,适合开发/测试环境使用。

方案B:扩展函数合并栈信息(无全局配置依赖)

如果不想开启全局调试,或者需要生产环境稳定保留栈信息,可以通过扩展函数将协程侧调用栈作为抑制异常附加到原异常上,不会产生额外输出,也不需要额外传参:

suspend fun <T> CompletionStage<T>.awaitWithFullStack(): T {
    // 预先捕获当前协程上下文的调用栈
    val coroutineStack = Throwable("Coroutine call trace")
    return try {
        await()
    } catch (e: Throwable) {
        // 将协程栈作为抑制异常添加到原异常中,不会覆盖原有根因
        e.addSuppressed(coroutineStack)
        throw e
    }
}

使用该扩展函数替换原有await()调用即可,捕获到异常打印时会自动输出原始异步栈+协程侧完整调用栈,上层不需要做任何额外处理。

内容的提问来源于stack exchange,提问作者Kirill Kosolapov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 03:15:01