diff --git a/kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImpl.kt index 2d048ac71a..960c064309 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImpl.kt @@ -28,7 +28,17 @@ internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExce internal actual fun propagateExceptionFinalResort(exception: Throwable) { // use the thread's handler val currentThread = Thread.currentThread() - currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) + val exceptionHandler = currentThread.uncaughtExceptionHandler + try { + exceptionHandler.uncaughtException(currentThread, exception) + } catch (_: Throwable) { + /* Do nothing. + * From https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html : + * > Any exception thrown by this method will be ignored by the Java Virtual Machine. + * + * This means the authors of the thread exception handlers have the right to throw exceptions indiscriminately. + * We have no further channels for propagating the fatal exception, so we give up. */ + } } // This implementation doesn't store a stacktrace, which is good because a stacktrace doesn't make sense for this. diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt index 2e3519902c..8433581be2 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/CoroutineExceptionHandlerJvmTest.kt @@ -8,28 +8,15 @@ import kotlin.test.* class CoroutineExceptionHandlerJvmTest : TestBase() { - private val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - private lateinit var caughtException: Throwable - - @Before - fun setUp() { - Thread.setDefaultUncaughtExceptionHandler({ _, e -> caughtException = e }) - } - - @After - fun tearDown() { - Thread.setDefaultUncaughtExceptionHandler(exceptionHandler) - } - @Test fun testFailingHandler() = runBlocking { expect(1) - val job = GlobalScope.launch(CoroutineExceptionHandler { _, _ -> throw AssertionError() }) { - expect(2) - throw TestException() + val caughtException = catchingUncaughtException { + GlobalScope.launch(CoroutineExceptionHandler { _, _ -> throw AssertionError() }) { + expect(2) + throw TestException() + }.join() } - - job.join() assertIs(caughtException) assertIs(caughtException.cause) assertIs(caughtException.suppressed[0]) @@ -40,12 +27,47 @@ class CoroutineExceptionHandlerJvmTest : TestBase() { @Test fun testLastDitchHandlerContainsContextualInformation() = runBlocking { expect(1) - GlobalScope.launch(CoroutineName("last-ditch")) { - expect(2) - throw TestException() - }.join() + val caughtException = catchingUncaughtException { + GlobalScope.launch(CoroutineName("last-ditch")) { + expect(2) + throw TestException() + }.join() + } assertIs(caughtException) assertContains(caughtException.suppressed[0].toString(), "last-ditch") finish(3) } + + @Test + fun testFailingUncaughtExceptionHandler() = runBlocking { + expect(1) + withUncaughtExceptionHandler({ _, e -> + expect(3) + throw TestException("uncaught") + }) { + launch(Job()) { + expect(2) + throw TestException("to be reported") + }.join() + } + finish(4) + } +} + +private inline fun catchingUncaughtException(action: () -> Unit): Throwable? { + var caughtException: Throwable? = null + withUncaughtExceptionHandler({ _, e -> caughtException = e }) { + action() + } + return caughtException +} + +private inline fun withUncaughtExceptionHandler(handler: Thread.UncaughtExceptionHandler, action: () -> T): T { + val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(handler) + try { + return action() + } finally { + Thread.setDefaultUncaughtExceptionHandler(exceptionHandler) + } }