diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt index 4c8c81b8db..dd39210d25 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt @@ -27,6 +27,24 @@ internal actual fun initializeDefaultExceptionHandlers() { CoroutineExceptionHandler } +/** + * Private exception without stacktrace that is added to suppressed exceptions of the original exception + * when it is reported to the last-ditch current thread 'uncaughtExceptionHandler'. + * + * The purpose of this exception is to add an otherwise inaccessible diagnostic information and to + * be able to poke the failing coroutine context in the debugger. + */ +private class DiagnosticCoroutineContextException(private val context: CoroutineContext) : RuntimeException() { + override fun getLocalizedMessage(): String { + return context.toString() + } + + override fun fillInStackTrace(): Throwable { + // Prevent Android <= 6.0 bug, #1866 + stackTrace = emptyArray() + return this + } +} internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) { // use additional extension handlers @@ -42,5 +60,8 @@ internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exce // use thread's handler val currentThread = Thread.currentThread() + // addSuppressed is never user-defined and cannot normally throw with the only exception being OOM + // we do ignore that just in case to definitely deliver the exception + runCatching { exception.addSuppressed(DiagnosticCoroutineContextException(context)) } currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) } diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt index cea9713f4a..2095f14886 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt @@ -39,4 +39,16 @@ class CoroutineExceptionHandlerJvmTest : TestBase() { finish(3) } + + @Test + fun testLastDitchHandlerContainsContextualInformation() = runBlocking { + expect(1) + GlobalScope.launch(CoroutineName("last-ditch")) { + expect(2) + throw TestException() + }.join() + assertTrue(caughtException is TestException) + assertContains(caughtException.suppressed[0].toString(), "last-ditch") + finish(3) + } }