Skip to content

Commit d209433

Browse files
authored
Ignore exceptions thrown from Thread.UncaughtExceptionHandler (#4525)
Fixes #4516 Background ---------- Whenever an *uncaught* exception happens in a coroutine, it gets reported to the `CoroutineExceptionHandler`. See <https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler>. However, if it's not installed, a platform-specific handler is used. On the JVM, this means invoking the thread's `UncaughtExceptionHandler`, which logs the exception to the console by default, but can be configured to do other things (for example, on Android, it will crash the application). Problem ------- User-specified `UncaughtExceptionHandler` instances are allowed to throw exceptions. Java's documentation says so (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 a user is allowed to write a throwing `UncaughtExceptionHandler`, and the caller has to deal with it. In our implementation, however, we are simply invoking the exception handler as a plain function, and if that function throws an exception, we allow this exception to propagate to the coroutine machinery, causing it to fail. Solution -------- To comply with the contract defined for `UncaughtExceptionHandler`, we also ignore the exceptions thrown from there.
1 parent fdb01da commit d209433

File tree

2 files changed

+62
-27
lines changed

2 files changed

+62
-27
lines changed

kotlinx-coroutines-core/jvm/src/internal/CoroutineExceptionHandlerImpl.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@ internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExce
2828
internal actual fun propagateExceptionFinalResort(exception: Throwable) {
2929
// use the thread's handler
3030
val currentThread = Thread.currentThread()
31-
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
31+
val exceptionHandler = currentThread.uncaughtExceptionHandler
32+
try {
33+
exceptionHandler.uncaughtException(currentThread, exception)
34+
} catch (_: Throwable) {
35+
/* Do nothing.
36+
* From https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html :
37+
* > Any exception thrown by this method will be ignored by the Java Virtual Machine.
38+
*
39+
* This means the authors of the thread exception handlers have the right to throw exceptions indiscriminately.
40+
* We have no further channels for propagating the fatal exception, so we give up. */
41+
val breakpoint = "Put a breakpoint here"
42+
}
3243
}
3344

3445
// This implementation doesn't store a stacktrace, which is good because a stacktrace doesn't make sense for this.

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

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,68 @@ import kotlin.test.*
88

99
class CoroutineExceptionHandlerJvmTest : TestBase() {
1010

11-
private val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
12-
private lateinit var caughtException: Throwable
13-
14-
@Before
15-
fun setUp() {
16-
Thread.setDefaultUncaughtExceptionHandler({ _, e -> caughtException = e })
17-
}
18-
19-
@After
20-
fun tearDown() {
21-
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
22-
}
23-
2411
@Test
25-
fun testFailingHandler() = runBlocking {
12+
fun testThrowingHandler() = runBlocking {
2613
expect(1)
27-
val job = GlobalScope.launch(CoroutineExceptionHandler { _, _ -> throw AssertionError() }) {
28-
expect(2)
29-
throw TestException()
14+
val caughtException = catchingUncaughtException {
15+
GlobalScope.launch(CoroutineExceptionHandler { _, _ -> throw TestException2() }) {
16+
expect(2)
17+
throw TestException()
18+
}.join()
3019
}
31-
32-
job.join()
3320
assertIs<RuntimeException>(caughtException)
34-
assertIs<AssertionError>(caughtException.cause)
21+
assertIs<TestException2>(caughtException.cause)
3522
assertIs<TestException>(caughtException.suppressed[0])
3623

3724
finish(3)
3825
}
3926

4027
@Test
41-
fun testLastDitchHandlerContainsContextualInformation() = runBlocking {
28+
fun testLastResortHandlerContainsContextualInformation() = runBlocking {
4229
expect(1)
43-
GlobalScope.launch(CoroutineName("last-ditch")) {
44-
expect(2)
45-
throw TestException()
46-
}.join()
30+
val caughtException = catchingUncaughtException {
31+
GlobalScope.launch(CoroutineName("last-resort")) {
32+
expect(2)
33+
throw TestException()
34+
}.join()
35+
}
4736
assertIs<TestException>(caughtException)
48-
assertContains(caughtException.suppressed[0].toString(), "last-ditch")
37+
assertContains(caughtException.suppressed[0].toString(), "last-resort")
4938
finish(3)
5039
}
40+
41+
@Test
42+
fun testThrowingUncaughtExceptionHandler() = runBlocking {
43+
expect(1)
44+
withUncaughtExceptionHandler({ _, e ->
45+
// should be above the `expect` so that we can observe the failure
46+
assertIs<TestException>(e)
47+
expect(3)
48+
throw TestException("will not be reported")
49+
}) {
50+
launch(Job()) {
51+
expect(2)
52+
throw TestException("will be passed to the uncaught exception handler")
53+
}.join()
54+
}
55+
finish(4)
56+
}
57+
}
58+
59+
private inline fun catchingUncaughtException(action: () -> Unit): Throwable? {
60+
var caughtException: Throwable? = null
61+
withUncaughtExceptionHandler({ _, e -> caughtException = e }) {
62+
action()
63+
}
64+
return caughtException
65+
}
66+
67+
private inline fun <T> withUncaughtExceptionHandler(handler: Thread.UncaughtExceptionHandler, action: () -> T): T {
68+
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
69+
Thread.setDefaultUncaughtExceptionHandler(handler)
70+
try {
71+
return action()
72+
} finally {
73+
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
74+
}
5175
}

0 commit comments

Comments
 (0)