Serendipitous Epiphany
Serendipitous Epiphany

Reputation: 141

Why do suspending functions throw exceptions in finally

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

Answers (2)

Marko Topolnik
Marko Topolnik

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

functionaldude
functionaldude

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

Related Questions