-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Open
Description
Documentation of CoroutineExceptionHandler states:
An optional element in the coroutine context to handle uncaught exceptions.
...
A coroutine that was created using [async][CoroutineScope.async] always catches all its exceptions and represents them in the resulting [Deferred] object, so it cannot result in uncaught exceptions. - reference
The above states that: async
cannot create uncaught exceptions.
However: IF async
throws an exception BEFORE we called await
on the deferred object, THEN uncaught exception happens BUT CoroutineExceptionHandler
does not appear to be invoked.
Code to reproduce
Code self contained
package com.glassthought.sandbox
import kotlinx.coroutines.*
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
fun main(): Unit {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
runBlocking(CoroutineName("CoroutineExceptionHandler")) {
printlnWithTime("Caught exception in CoroutineExceptionHandler: ${ex::class.simpleName} with message=[${ex.message}].")
}
}
val mainJob = Job()
val scope = CoroutineScope(mainJob + coroutineExceptionHandler)
scope.launch(CoroutineName("SpawnedFromMain")) {
actionWithMsg("foo", { foo(scope) })
}
runBlocking {
actionWithMsg("mainJob.join()", { mainJob.join() })
}
}
private suspend fun foo(scope: CoroutineScope) {
val deferred = scope.async(CoroutineName("async-1")) {
actionWithMsg("throw-exception", {
throw MyRuntimeException.create("exception-from-async-before-it-got-to-await")
}, delayDuration = 500.milliseconds)
}
printlnWithTime("Just launched co-routines.")
actionWithMsg(
"deferred.await()",
{
deferred.await()
},
delayDuration = 1.seconds
)
}
suspend fun <T> actionWithMsg(
actionName: String,
action: suspend () -> T,
delayDuration: Duration = Duration.ZERO
): T {
val hasDelay = delayDuration > Duration.ZERO
if (hasDelay) {
printlnWithTime("action=[$actionName] is being delayed for=[${delayDuration.inWholeMilliseconds} ms] before starting.")
delay(delayDuration)
}
printlnWithTime("action=[$actionName] is starting.")
var result: T
try {
result = action()
} catch (exc: Exception) {
// We are going to rethrow all exceptions, so CancellationException will also be rethrown.
// Therefore, we respect: [Cooperative Cancellation](http://www.glassthought.com/notes/3ha01u9931je002miy86vdo)
if (exc is CancellationException) {
printlnWithTime("Cancellation Exception - rethrowing.")
} else {
printlnWithTime("Finished action=[$actionName], it THREW exception of type=[${exc::class.simpleName}] we are rethrowing it.")
}
throw exc
}
printlnWithTime("Finished action=[$actionName].")
return result
}
class MyRuntimeException private constructor(msg: String) : RuntimeException(msg) {
companion object {
suspend fun create(msg: String): MyRuntimeException {
val exc = MyRuntimeException(msg)
printlnWithTime("throwing exception=[${exc::class.simpleName}] with msg=[${msg}]")
return exc
}
}
}
fun printlnWithTime(msg: String) {
println("[${Instant.now()}]: " + msg)
}
Original Code
package com.glassthought.sandbox
import com.glassthought.sandbox.util.out.impl.out
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
fun main(): Unit {
val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
runBlocking(CoroutineName("CoroutineExceptionHandler")) {
out.error("Caught exception in CoroutineExceptionHandler: ${ex::class.simpleName} with message=[${ex.message}].")
}
}
val mainJob = Job()
val scope = CoroutineScope(mainJob + coroutineExceptionHandler)
scope.launch(CoroutineName("SpawnedFromMain")) {
out.actionWithMsg("foo", { foo(scope) })
}
runBlocking {
out.actionWithMsg("mainJob.join()", { mainJob.join() })
}
}
private suspend fun foo(scope: CoroutineScope) {
val deferred = scope.async(CoroutineName("async-1")) {
out.actionWithMsg("throw-exception", {
throw MyRuntimeException.create("exception-from-async-before-it-got-to-await", out)
}, delayDuration = 500.milliseconds)
}
out.info("Just launched co-routines.")
out.actionWithMsg(
"deferred.await()",
{
deferred.await()
},
delayDuration = 1.seconds
)
}
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
[INFO][elapsed: 18ms][①][coroutname:@SpawnedFromMain#1] [->] action=[foo] is starting.
[INFO][elapsed: 18ms][⓶][coroutname:@coroutine#2] [->] action=[mainJob.join()] is starting.
[INFO][elapsed: 31ms][①][coroutname:@SpawnedFromMain#1] Just launched co-routines.
[INFO][elapsed: 35ms][①][coroutname:@SpawnedFromMain#1] [🐢] action=[deferred.await()] is being delayed for=[1000 ms] before starting.
[INFO][elapsed: 35ms][⓷][coroutname:@async-1#3] [🐢] action=[throw-exception] is being delayed for=[500 ms] before starting.
[INFO][elapsed: 535ms][⓷][coroutname:@async-1#3] [->] action=[throw-exception] is starting.
[WARN][elapsed: 557ms][⓷][coroutname:@async-1#3] 💥 throwing exception=[MyRuntimeException] with msg=[exception-from-async-before-it-got-to-await]
[WARN][elapsed: 557ms][⓷][coroutname:@async-1#3] [<-][💥] Finished action=[throw-exception], it THREW exception of type=[MyRuntimeException] we are rethrowing it.
[INFO][elapsed: 568ms][①][coroutname:@SpawnedFromMain#1] [<-][🫡] Cancellation Exception - rethrowing.
[INFO][elapsed: 569ms][⓶][coroutname:@coroutine#2] [<-] Finished action=[mainJob.join()].
Output colored per co-routine:

Metadata
Metadata
Assignees
Labels
No labels