I’m observing a strange result in my unit tests concerning the behavior of exception handling in coroutines.
When an exception E with a cause C is thrown from within a coroutine scope, and we catch E outside the scope, the cause of E is another exception E’ of the same type of E instead of its cause C. The cause C, however, can be found as the cause of E’.
This is better understandable through a concrete example. The following is a minimal test method that reproduces this behavior:
@Test
fun test() = runBlocking {
try {
coroutineScope {
// Somewhere within a coroutineScope, an exception is thrown
throw IllegalStateException("OOB", IndexOutOfBoundsException())
}
} catch (e: Exception) {
// The caught exception is an IllegalStateException. So far, so good.
println(e) // java.lang.IllegalStateException: OOB
// However, the cause of the caught is also an IllegalStateException.
// Where does it come from?
// We expected this to be an IndexOutOfBoundsException.
println(e.cause) // java.lang.IllegalStateException: OOB
// The IndexOutOfBoundsExceptions is the cause of the nested IllegalStateException.
println(e.cause?.cause) // java.lang.IndexOutOfBoundsException
}
}
For context, I’m using Kotlin 1.9.22 and the coroutines library 1.7.3.
This behavior is only present in unit tests. For instance, if the above code runs from a main
function, the behavior is what we would expect:
fun main() = runBlocking {
try {
coroutineScope {
throw IllegalStateException("OOB", IndexOutOfBoundsException())
}
} catch (e: Exception) {
println(e) // java.lang.IllegalStateException: OOB
println(e.cause) // java.lang.IndexOutOfBoundsException
println(e.cause?.cause) // null
}
}