skip to content

isActive / ensureActive / yield

To make computational loops cancellable you check isActive, call ensureActive() to throw, or call yield() to both check and give the dispatcher a turn. Knowing which of the three fits a given loop is the practical part.

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

questions

5

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

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

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

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