如何在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
相关产品推荐
相关产品推荐

