This code:
fun main() {
runBlocking {
try {
val deferred = async { throw Exception() }
deferred.await()
} catch (e: Exception) {
println("Caught $e")
}
}
println("Completed")
}
results in this output:
Caught java.lang.Exception
Exception in thread "main" java.lang.Exception
at org.mtopol.TestKt$main$1$deferred$1.invokeSuspend(test.kt:11)
...
This behavior doesn't make sense to me. The exception was caught and handled, and still it escapes to the top-level as an unhandled exception.
Is this behavior documented and expected? It violates all my intuitions on how exception handling is supposed to work.
I adapted this question from a thread on the Kotlin forum.
The Kotlin docs suggest using supervisorScope if we don't want to cancel all coroutines when one fails. So I can write
fun main() {
runBlocking {
supervisorScope {
try {
launch {
delay(1000)
println("Done after delay")
}
val job = launch {
throw Exception()
}
job.join()
} catch (e: Exception) {
println("Caught $e")
}
}
}
println("Completed")
}
The output is now
Exception in thread "main" java.lang.Exception
at org.mtopol.TestKt$main$2$1$job$1.invokeSuspend(test.kt:16)
...
at org.mtopol.TestKt.main(test.kt:8)
...
Done after delay
Completed
This, again, is not be the behavior I want. Here a launched coroutine failed with an unhandled exception, invalidating the work of other coroutines, but they proceed uninterrupted.
The behavior I would find reasonable is to spread cancellation when a coroutine fails in an unforeseen (i.e., unhandled) manner. Catching an exception from await means that there wasn't any global error, just a localized exception that is handled as a part of the business logic.