Skip to content

Commit f22604b

Browse files
committed
Recover stacktraces for no-dispatched continuations, so recovery works in 'suspend fun main' cases to further improve user experience
Fixes #1328
1 parent d100a3f commit f22604b

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

kotlinx-coroutines-core/common/src/Dispatched.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,14 @@ internal fun <T> DispatchedTask<T>.resume(delegate: Continuation<T>, useMode: In
307307
val state = takeState()
308308
val exception = getExceptionalResult(state)
309309
if (exception != null) {
310-
delegate.resumeWithExceptionMode(exception, useMode)
310+
/*
311+
* Recover stacktrace for non-dispatched tasks.
312+
* We usually do not recover stacktrace in a `resume` as all resumes go through `DispatchedTask.run`
313+
* and we recover stacktraces there, but this is not the case for a `suspend fun main()` that knows nothing about
314+
* kotlinx.coroutines and DispatchedTask
315+
*/
316+
val recovered = if (delegate is DispatchedTask<*>) exception else recoverStackTrace(exception, delegate)
317+
delegate.resumeWithExceptionMode(recovered, useMode)
311318
} else {
312319
delegate.resumeMode(getSuccessfulResult(state), useMode)
313320
}

kotlinx-coroutines-core/jvm/test/exceptions/StackTraceRecoveryTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package kotlinx.coroutines.exceptions
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.channels.*
9+
import kotlinx.coroutines.intrinsics.*
910
import kotlinx.coroutines.selects.*
1011
import org.junit.Test
1112
import java.util.concurrent.*
13+
import kotlin.coroutines.*
1214
import kotlin.test.*
1315

1416
/*
@@ -271,4 +273,40 @@ class StackTraceRecoveryTest : TestBase() {
271273
checkCycles(e)
272274
}
273275
}
276+
277+
278+
private suspend fun throws() {
279+
yield() // TCE
280+
throw RecoverableTestException()
281+
}
282+
283+
private suspend fun awaiter() {
284+
val task = GlobalScope.async(Dispatchers.Default, start = CoroutineStart.LAZY) { throws() }
285+
task.await()
286+
yield() // TCE
287+
}
288+
289+
@Test
290+
fun testNonDispatchedRecovery() {
291+
val await = suspend { awaiter() }
292+
293+
val barrier = CyclicBarrier(2)
294+
var exception: Throwable? = null
295+
await.startCoroutineUnintercepted(Continuation(EmptyCoroutineContext) {
296+
exception = it.exceptionOrNull()
297+
barrier.await()
298+
})
299+
300+
barrier.await()
301+
val e = exception
302+
assertNotNull(e)
303+
verifyStackTrace(e, "kotlinx.coroutines.RecoverableTestException\n" +
304+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.throws(StackTraceRecoveryTest.kt:280)\n" +
305+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$throws\$1.invokeSuspend(StackTraceRecoveryTest.kt)\n" +
306+
"\t(Coroutine boundary)\n" +
307+
"\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
308+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.awaiter(StackTraceRecoveryTest.kt:285)\n" +
309+
"\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testNonDispatchedRecovery\$await\$1.invokeSuspend(StackTraceRecoveryTest.kt:291)\n" +
310+
"Caused by: kotlinx.coroutines.RecoverableTestException")
311+
}
274312
}

0 commit comments

Comments
 (0)