Introduce private DiagnosticCoroutineContextException and add it to the original exception prior to passing it to the Thread.currentThread().uncaughtExceptionHandler() (#3170)
Fixes #3153
diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
index 4c8c81b..dd39210 100644
--- a/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
+++ b/kotlinx-coroutines-core/jvm/src/CoroutineExceptionHandlerImpl.kt
@@ -27,6 +27,24 @@
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 @@
// 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 cea9713..2095f14 100644
--- a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
+++ b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt
@@ -39,4 +39,16 @@
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)
+ }
}