Cooperative cancellation

Coroutines Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Spot the bug

The job is cancelled after 1 second, but the work runs to the end. Which line is the problem?

  1. val job = scope.launch(Dispatchers.Default) {
  2. var total = 0L
  3. for (i in 1..5_000_000_000L) {
  4. total += i % 7
  5. }
  6. log("total $total")
  7. }
  8. delay(1_000)
  9. job.cancel()
Hint 1 of 2

Where would a CancellationException be thrown?

Hint 2 of 2

Does anything in the loop suspend?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 3 for (i in 1..5_000_000_000L) {

The loop never suspends or checks for cancellation, so the cancel flag is never read. Add ensureActive() every so often inside the loop.

30-second answer

Cancellation in coroutines is cooperative. Cancelling a job sets a flag and makes the next suspension point throw CancellationException. Code that never suspends, like a tight CPU loop, never sees it. Check isActive, call ensureActive() or yield() inside long loops so the work can stop.

Practise saying it · 35 s. No recording, just a timer

Explain why cancellation is cooperative.

Say your answer out loud, then check which of these points you covered:

  • Cancel sets a flag
  • Suspension points throw CancellationException
  • Tight loops must check isActive or call ensureActive
  • Clean up in finally

Deep dive

All suspending functions in kotlinx.coroutines are cancellable: delay, withContext, await, channel and Flow operators.

ensureActive() throws if cancelled; yield() also lets other coroutines run on a busy dispatcher.

Blocking calls such as Thread.sleep or blocking I/O ignore cancellation until they return.

Clean up in finally. If cleanup must suspend, wrap it in withContext(NonCancellable).

A cancellable loop Compile-checked · Kotlin 2.4.21
suspend fun checksum(chunks: List<ByteArray>): Long = withContext(Dispatchers.Default) {
    var sum = 0L
    for (chunk in chunks) {
        ensureActive()
        for (b in chunk) sum += b
    }
    sum
}

Common mistakes

  • Expecting job.cancel() to stop a loop that never suspends.
  • Calling suspend functions in finally after cancellation and wondering why they throw at once.
  • Using Thread.sleep inside coroutines.

Senior take & follow-ups

Explain the mechanism (a flag plus exceptions at suspension points), then the three tools: isActive, ensureActive and yield.

Follow-up questions

Think of your answer first, then open the hint.

  1. How do you make a blocking library call cancellable?

    Show hint

    runInterruptible, which interrupts the thread on cancellation.

  2. What does job.cancelAndJoin() add?

    Show hint

    It waits until the job has actually finished cleaning up.

Compare on iOS: Cooperative cancellation Swift task cancellation is cooperative too, checked with Task.checkCancellation().

Suggest a fix if something here is wrong or out of date.