skip to content

Cancellation

Cancellation in coroutines is cooperative: it takes effect at suspension points, so code that never suspends never notices. This is one of the most common sources of real bugs and therefore of interview questions.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

19

You launch a coroutine that runs a tight CPU-bound while-loop, then call cancel() on its Job. The loop keeps running. Why, and what is the simplest fix?

level: juniorimportance: must knowfreq 70%

answer

  1. Cancellation is cooperative, not forced
  2. cancel() only sets state
  3. CPU loop has no suspension point
  4. while (isActive) is the simplest fix
  5. ensureActive/yield also work

basics

~10 s

Cancellation is cooperative: the coroutine only stops if it checks for cancellation. A pure CPU loop never checks, so it ignores cancel(). Add a check inside the loop, such as while (isActive).

solid answer

~40 s

Kotlin coroutine cancellation is cooperative. cancel() just sets the Job's state to cancelling; nothing forcibly stops the running code. Suspend functions in kotlinx.coroutines (delay, yield, etc.) cooperate by throwing CancellationException when resumed after cancel. A tight CPU loop calls no such suspend point, so it never observes cancellation and runs to completion. The simplest fix is to make the loop check the cancellation state: use `while (isActive)` (isActive is a CoroutineScope/CoroutineContext extension that is false once cancelled), or call `ensureActive()` / `yield()` inside the loop body. isActive is non-throwing and just stops the loop; ensureActive() throws CancellationException; yield() also throws and additionally lets other coroutines on the dispatcher run.

code

kotlin · 9 lines
kotlin
val job = launch(Dispatchers.Default) {
    var i = 0L
    while (isActive) {   // checks cancellation each iteration
        i++
    }
    println("stopped at $i")
}
delay(50)
job.cancelAndJoin()  // now the loop actually stops

go deeper

for a junior

States cancellation is cooperative and adds while (isActive) to fix the loop.

for a middle

Explains cancel() only sets state and lists isActive vs ensureActive vs yield with their throw/non-throw behavior.

for a senior

Connects to how suspend functions act as check points and why CancellationException must not be swallowed.

for a principal

Discusses structured-concurrency implications, dispatcher fairness via yield, and designing long compute loops with periodic checks and budgets.

## Why the loop ignores cancel() **Cooperative cancellation** means a coroutine is never killed from the outside. Calling `job.cancel()` only moves the `Job` into the *Cancelling* state and stores a `CancellationException`. The coroutine actually stops only when its code reaches a **cancellation check point** and observes that state. The built-in suspend functions in `kotlinx.coroutines` (`delay`, `yield`, `withContext`, channel/Flow operators, etc.) are the usual check points — when the coroutine is resumed after a cancel, they throw `CancellationException`. A tight loop like this contains no such point: ```kotlin val job = launch(Dispatchers.Default) { var i = 0L while (i < Long.MAX_VALUE) { i++ } // no suspension, no check } job.cancel() // sets state, but the loop never notices ``` So the loop runs to the end regardless of `cancel()`. ## How to make it cancellable Insert a cancellation check inside the loop. Three options: - **`isActive`** — a Boolean extension on `CoroutineScope`/`CoroutineContext`. It is `true` while active and `false` once cancelled. Non-throwing; you just exit the loop. ```kotlin while (isActive) { /* work */ } ``` - **`ensureActive()`** — checks the same state but **throws** `CancellationException` if cancelled, so the coroutine unwinds like any other suspend function would. - **`yield()`** — a `suspend` function that checks cancellation (throws if cancelled) **and** yields the thread so other coroutines can run. ## Key terms - **`Job`**: handle to a coroutine's lifecycle; `cancel()` requests cancellation. - **`CancellationException`**: the signal used to unwind a cancelled coroutine; it is treated specially and not logged as an error. - **CPU-bound**: work that keeps the thread busy computing (no I/O, no suspension), which is exactly the case that needs manual checks.

  • Why is delay() cancellable but your while-loop is not?
    delay() is a suspend function that registers a cancellation handler; when the Job is cancelled it resumes with CancellationException. A plain loop suspends nowhere, so there is nothing to throw.
  • Does isActive throw?
    No. isActive just returns false once cancelled; you decide how to exit. ensureActive() and yield() throw.

cancel() is like raising your hand to ask the runner to stop — they only stop if they occasionally look at you (the check inside the loop).

saying these in an interview costs you the question

  • Claiming cancel() forcibly kills the coroutine / interrupts the thread
  • Saying you must use Thread.stop() or interrupt()
  • Not knowing cancellation is cooperative
  • Thinking a CPU loop is automatically cancellable
  • Catching and swallowing CancellationException as a fix

context

open as a page

In Kotlin coroutines, what actually happens when you call Job.cancel()? Does the coroutine stop immediately?

level: juniorimportance: must knowfreq 75%

basics

~10 s

No. cancel() just marks the job as cancelled. The coroutine actually stops the next time it pauses at a suspending call. If it never pauses, it keeps running.

open as a page

Why can a suspending cleanup call inside a finally block fail to run after a coroutine is cancelled, and how does NonCancellable fix it?

level: juniorimportance: must knowfreq 55%

basics

~10 s

When a coroutine is cancelled, trying to suspend again (like awaiting a call) in finally immediately throws cancellation, so cleanup is skipped. Wrapping that cleanup in withContext(NonCancellable) lets it suspend and complete.

open as a page

What do withTimeout and withTimeoutOrNull do in Kotlin coroutines, and how do they differ?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Both run a block of code with a time limit. If the work takes too long, withTimeout throws an error, while withTimeoutOrNull just returns null. Use them so slow operations don't hang forever.

open as a page

Compare isActive, ensureActive() and yield() for making a loop cancellable. When would you pick each?

level: middleimportance: must knowfreq 60%

basics

~20 s

isActive is a Boolean you test to exit the loop without throwing. ensureActive() throws CancellationException if cancelled. yield() throws too and also lets other coroutines run. Pick by whether you want to throw and/or reschedule.

open as a page

A loop uses ensureActive() but a broad try/catch around it swallows the exception, so the coroutine never stops. What is happening and how do you fix it correctly?

level: middleimportance: must knowfreq 45%

basics

~10 s

ensureActive() throws CancellationException, but a catch (e: Exception) swallows it, so the loop keeps running. Fix it by rethrowing CancellationException (or catching only the exceptions you actually expect).

open as a page

What goes wrong if you catch a broad exception type around a suspending call and swallow it? How should you handle CancellationException?

level: middleimportance: must knowfreq 65%

basics

~10 s

CancellationException is how a coroutine learns it was cancelled. If you catch it and don't rethrow, the coroutine thinks the error was handled and keeps running, so cancellation silently stops working.

open as a page

Show the correct pattern for running a suspending cleanup operation (e.g. releasing a remote lock) after cancellation, and explain why withContext(NonCancellable) is required.

level: middleimportance: must knowfreq 50%

basics

~10 s

Put the suspending cleanup inside finally, and wrap just that cleanup in withContext(NonCancellable). That makes the cleanup ignore the cancellation long enough to finish, then cancellation resumes propagating.

open as a page

Why is catching exceptions inside a withTimeout block dangerous, and how should you handle TimeoutCancellationException correctly?

level: middleimportance: must knowfreq 55%

basics

~10 s

The timeout works by throwing a special cancellation exception. If your try/catch swallows it, the timeout stops working and cleanup may break. Always let cancellation exceptions pass through, or use withTimeoutOrNull instead.

open as a page

How does a suspending function like delay() detect cancellation, and what does 'cancellation is checked on resume' mean mechanically?

level: middleimportance: should knowfreq 55%

basics

~10 s

Cancellable suspend functions register with the Job. When the Job is cancelled, their pending wait is aborted and they resume by throwing CancellationException instead of returning normally.

open as a page

How do withTimeout and withTimeoutOrNull interact with structured concurrency and nested deadlines?

level: middleimportance: should knowfreq 30%

basics

~10 s

A timeout cancels everything started inside its block, including child coroutines. If you nest timeouts, the shorter effective deadline wins. The timeout block behaves like a normal coroutine scope.

open as a page

You have a CPU-bound loop with tens of millions of cheap iterations. How do you make it cancellable without crushing throughput, and why not check on every iteration?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Check cancellation periodically, e.g. every few thousand iterations, instead of every one. Frequent checks add overhead and, for yield(), expensive rescheduling, so you balance responsiveness against throughput.

open as a page

After calling job.cancel(), can the coroutine still execute code? Explain the Cancelling vs Cancelled states and what cancelAndJoin() guarantees.

level: seniorimportance: should knowfreq 30%

basics

~10 s

Yes. cancel() returns right away while the coroutine is still finishing — running finally blocks and cleanup. It's fully done only after it reaches the Cancelled state, which cancelAndJoin() waits for.

open as a page

Why did Kotlin choose cooperative cancellation instead of forcibly interrupting coroutines? What are the trade-offs and implications for blocking work?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Forcibly killing running code can leave data and locks in a broken state. Cooperative cancellation lets code stop at safe points and clean up. The cost is that you must add cancellation checks to long non-suspending work.

open as a page

How does withContext(NonCancellable) interact with the coroutine context and Job hierarchy? What is and isn't suppressed inside the block?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Inside the block, only the Job is swapped for NonCancellable, so normal cancellation is ignored. The dispatcher and other context stay the same, and timeout-based cancellation (withTimeout) inside the block still works.

open as a page

When wrapping resource acquisition in withTimeout, how can a timeout leak resources, and how do you prevent it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

If a timeout cancels the code right after a resource is opened but before you store or release it, the resource can leak. Acquire and release inside try/finally, and make sure cleanup still runs during cancellation.

open as a page

Why might withTimeout fail to cancel a long-running block on time, and how do you make CPU-bound work respect the deadline?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Timeouts only kick in when the code pauses at a suspend point. Pure computation that never pauses ignores the deadline. To fix it, periodically check if the coroutine is still active, or yield, so cancellation can take effect.

open as a page

What does isActive return when read outside any coroutine, and why can that bite you when extracting CPU loops into helper functions?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

If there is no Job in scope, isActive returns true. So a loop moved into a plain function with no coroutine context will think it is always active and never stop on cancel.

open as a page

What are the dangers of overusing NonCancellable, and what alternatives exist for resource cleanup that don't suppress cancellation?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Overusing NonCancellable makes coroutines ignore cancellation, so they can hang shutdown or leak work. For non-suspending cleanup, plain try/finally or use{} is enough; reserve NonCancellable for short suspend cleanup and bound it with a timeout.

open as a page