Problem
I am using kotlinx.coroutines.future.FutureKt#await to await for an async code. But when any exception is thrown from this async code, the Exception doesn't contain full stack call. E.g.:
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()
}
}
This results in the following output:
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)
The stack trace only has the runAsync method part.
Solution #1
While trying to workaround this problem, I first thought of catching it just outside of await:
suspend fun <T> CompletionStage<T>.awaitWithException(): T {
try {
return await()
} catch (e: Exception) {
throw Exception(e)
}
}
java.lang.Exception: java.lang.Exception
at TestKotlinKt.awaitWithException(TestKotlin.kt:36)
at TestKotlinKt$awaitWithException$1.invokeSuspend(TestKotlin.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
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 TestKotlinKt.myFun1Blocking(TestKotlin.kt:23)
at TestKotlinKt.main(TestKotlin.kt:16)
at TestKotlinKt.main(TestKotlin.kt)
Caused by: java.lang.Exception
at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:43)
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)
Better, but I still lost the myFun2Suspend call in the stack.
Solution #2
Then I tried saving the stacktrace just before await():
suspend fun <T> CompletionStage<T>.awaitWithException(printStream: PrintStream): T {
val throwable = Throwable("Await Exception")
try {
return await()
} catch (e: Exception) {
throwable.printStackTrace(printStream)
throw e
}
}
java.lang.Throwable: Await Exception
at TestKotlinKt.awaitWithException(TestKotlin.kt:43)
at TestKotlinKt.myFun2Suspend(TestKotlin.kt:31)
at TestKotlinKt$myFun1Blocking$1.invokeSuspend(TestKotlin.kt:26)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
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 TestKotlinKt.myFun1Blocking(TestKotlin.kt:25)
at TestKotlinKt.main(TestKotlin.kt:18)
at TestKotlinKt.main(TestKotlin.kt)
AsyncException
at TestKotlinKt.runAsync$lambda-0(TestKotlin.kt:55)
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)
Now I see everything.
All in all, my solution seems to be very hacky. Is there something better?