Reputation: 141
As the title says, why do suspending functions throw exceptions in finally
?
With regular functions, the finally
-block executes all of them:
import kotlinx.coroutines.*
fun main() {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
launch {
// the first child
try {
println("inside try")
delay(1000)
} finally {
println("Children are cancelled, but exception is not handled until all children terminate")
Thread.sleep(1000)
println("thread.sleep executed")
//foo()
println("The first child finished its non cancellable block")
}
}
launch {
// the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
Thread.sleep(1000000)
println("complete")
}
Here, for example, when I do Thread.sleep(1000)
it prints:
"The first child finished its non cancellable block"
but if I change that line to delay(1000)
, it does not.
From my understanding, in a finally
-block, the exception, if it exists, is thrown after executing the entire block.
But in this case, delay
causes this exception to be thrown early.
On the other hand, Thread.sleep
does not.
Can someone help explain?
Upvotes: 2
Views: 1674
Reputation: 200158
From my understanding, in a
finally
-block, the exception, if it exists, is thrown after executing the entire block.
This is not true. If a finally
block throws an exception, it causes the finally
block to terminate abruptly with that exception. Any exception that was thrown within try
is thus discarded. This is exactly what happens in your case: the finally
block of the first child coroutine receives a CancellationException
on the delay(1000)
line. Thread.sleep(1000)
is a blocking, non-cancellable function, therefore it doesn't observe the cancellation.
You probably mixed up this with the fact that, if the try
block throws an exception, then first the complete finally
block is executed before throwing the exception. The finally
block is required to complete normally in order for this to happen.
So I believe you aren't describing any difference in the behavior of plain and suspendable functions.
Upvotes: 0
Reputation: 732
Suspending functions in Kotlin work differently than blocking function.
When you cancel a Job
, at the first suspension after the cancellation the execution will be stopped, even if you are in a finally
block. If you use Thread.sleep(1000)
instead of delay(1000)
in your finally
block, there are no suspensions taking place, because Thread.sleep()
is blocking, not suspending, so your whole finally
block gets executed.
Note that using blocking functions inside of suspending functions is an anti-pattern and should be avoided!!
To achieve this desired behavior without using blocking functions, use withContext(NonCancellable) {...}
as described here.
Your example code should look like this:
fun main() {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
launch {
// the first child
try {
println("inside try")
delay(1000000)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(1000) // This suspension cannot be cancelled
println("delay executed")
//foo()
println("The first child finished its non cancellable block")
}
}
}
launch {
// the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
Thread.sleep(1000000)
println("complete")
}
The output:
inside try
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
delay executed
The first child finished its non cancellable block
Caught java.lang.ArithmeticException
Upvotes: 5