Skip to content

Commit 409ed26

Browse files
committed
Handle exception thrown by user-defined exception handler via global exception handler, so such exception can't break coroutines machinery
Fixes #562
1 parent f157cec commit 409ed26

File tree

4 files changed

+72
-15
lines changed

4 files changed

+72
-15
lines changed

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

+16-13
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ internal expect fun handleCoroutineExceptionImpl(context: CoroutineContext, exce
1818
*
1919
* If there is a [Job] in the context and it's not a [caller], then [Job.cancel] is invoked.
2020
* If invocation returned `true`, method terminates: now [Job] is responsible for handling an exception.
21-
* Otherwise, If there is [CoroutineExceptionHandler] in the context, it is used.
22-
* Otherwise all instances of [CoroutineExceptionHandler] found via [ServiceLoader] and [Thread.uncaughtExceptionHandler] are invoked
23-
*
21+
* Otherwise, If there is [CoroutineExceptionHandler] in the context, it is used. If it throws an exception during handling
22+
* or is absent, all instances of [CoroutineExceptionHandler] found via [ServiceLoader] and [Thread.uncaughtExceptionHandler] are invoked
2423
* todo: Deprecate/hide this function.
2524
*/
2625
@JvmOverloads // binary compatibility
@@ -39,21 +38,25 @@ private fun handleExceptionViaJob(context: CoroutineContext, exception: Throwabl
3938
}
4039

4140
internal fun handleExceptionViaHandler(context: CoroutineContext, exception: Throwable) {
41+
// Invoke exception handler from the context if present
4242
try {
43-
// Invoke exception handler from the context if present
4443
context[CoroutineExceptionHandler]?.let {
4544
it.handleException(context, exception)
4645
return
4746
}
48-
// If handler is not present in the context, fallback to the global handler
49-
handleCoroutineExceptionImpl(context, exception)
50-
} catch (handlerException: Throwable) {
51-
// simply rethrow if handler threw the original exception
52-
if (handlerException === exception) throw exception
53-
// handler itself crashed for some other reason -- that is bad -- keep both
54-
throw RuntimeException("Exception while trying to handle coroutine exception", exception).apply {
55-
addSuppressedThrowable(handlerException)
56-
}
47+
} catch (t: Throwable) {
48+
handleCoroutineExceptionImpl(context, handlerException(exception, t))
49+
return
50+
}
51+
52+
// If handler is not present in the context or exception was thrown, fallback to the global handler
53+
handleCoroutineExceptionImpl(context, exception)
54+
}
55+
56+
internal fun handlerException(originalException: Throwable, thrownException: Throwable): Throwable {
57+
if (originalException === thrownException) return originalException
58+
return RuntimeException("Exception while trying to handle coroutine exception", thrownException).apply {
59+
addSuppressedThrowable(originalException)
5760
}
5861
}
5962

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,10 @@ internal open class JobSupport constructor(active: Boolean) : Job, SelectClause0
877877
* Note: This function attaches a special ChildNode object. This node object
878878
* is handled in a special way on completion on the coroutine (we wait for all of them) and
879879
* is handled specially by invokeOnCompletion itself -- it adds this node to the list even
880-
* if the job is already failing.
880+
* if the job is already failing. For "failing" state child is attached under state lock.
881+
* It's required to properly wait all children before completion and provide linearizable hierarchy view:
882+
* If child is attached when job is failing, such child will receive immediate cancellation exception,
883+
* but parent *will* wait for that child before completion and will handle its exception.
881884
*/
882885
return invokeOnCompletion(onFailing = true, handler = ChildJob(this, child).asHandler) as ChildHandle
883886
}

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ private val handlers: List<CoroutineExceptionHandler> = CoroutineExceptionHandle
2121
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
2222
// use additional extension handlers
2323
for (handler in handlers) {
24-
handler.handleException(context, exception)
24+
try {
25+
handler.handleException(context, exception)
26+
} catch (t: Throwable) {
27+
// Use thread's handler if custom handler failed to handle exception
28+
val currentThread = Thread.currentThread()
29+
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
30+
}
2531
}
32+
2633
// use thread's handler
2734
val currentThread = Thread.currentThread()
2835
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.experimental.exceptions
6+
7+
import kotlinx.coroutines.experimental.*
8+
import org.junit.*
9+
import org.junit.Test
10+
import kotlin.test.*
11+
12+
class CoroutineExceptionHandlerJvmTest : TestBase() {
13+
14+
private val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
15+
private lateinit var caughtException: Throwable
16+
17+
@Before
18+
fun setUp() {
19+
Thread.setDefaultUncaughtExceptionHandler({ _, e -> caughtException = e })
20+
}
21+
22+
@After
23+
fun tearDown() {
24+
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler)
25+
}
26+
27+
@Test
28+
fun testFailingHandler() = runBlocking {
29+
expect(1)
30+
val job = GlobalScope.launch(CoroutineExceptionHandler { _, _ -> throw AssertionError() }) {
31+
expect(2)
32+
throw TestException()
33+
}
34+
35+
job.join()
36+
assertTrue(caughtException is RuntimeException)
37+
assertTrue(caughtException.cause is AssertionError)
38+
assertTrue(caughtException.suppressed()[0] is TestException)
39+
40+
finish(3)
41+
}
42+
43+
private class TestException : Throwable()
44+
}

0 commit comments

Comments
 (0)