What does isActive return when read outside any coroutine, and why can that bite you when extracting CPU loops into helper functions?
answer
- No Job in context => isActive is true
- Plain helper loses coroutine context
- Make CPU helpers suspend
- Or use a CoroutineScope receiver
- Check must bind to the running Job
basics
~10 sIf 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.
solid answer
~40 s`isActive` is an extension on `CoroutineScope`/`CoroutineContext` that reads the `Job`'s state. When you read it where **no Job is present** — e.g. a top-level extension on a `CoroutineScope` you constructed wrongly, or inside a plain non-suspend helper that captured no context — it defaults to `true`. The danger appears when you extract a CPU loop into a helper: if the helper is a plain function (not `suspend`, no `CoroutineScope` receiver), `coroutineContext` isn't available, so people sometimes reach for the wrong `isActive` or hardcode `while (true)`, and cancellation silently stops working. Fixes: make the helper `suspend` (then use `coroutineContext.isActive`/`ensureActive()`/`yield()`), or give it a `CoroutineScope` receiver and use its `isActive`, or pass a cancellation check in. The general lesson: cancellation checks only work when bound to the *actual* running coroutine's Job.
code
kotlin · 10 lines// GOOD: suspend helper keeps the Job via coroutineContext
suspend fun crunch(total: Long): Long {
var acc = 0L
for (i in 0 until total) {
currentCoroutineContext().ensureActive() // real cancel check
acc += i
}
return acc
}
// launch { crunch(1_000_000) } now cancels properlygo deeper
Likely unaware isActive defaults to true outside a coroutine.
Knows isActive needs a Job and that suspend helpers carry context.
Explains the default-true trap, the extraction failure mode, and the suspend / receiver / passed-check fixes.
Establishes API conventions so cancellable compute is always suspend or scope-receiver, and reviews for ambient-isActive misuse.
## Two `isActive`s and the default - `CoroutineScope.isActive` and `CoroutineContext.isActive` read `this[Job]?.isActive`. - The implementation returns **`true` when there is no `Job`** in the context (`coroutineContext[Job]?.isActive ?: true`). The idea: "no job ⇒ nothing to cancel ⇒ treat as active." That default is the trap. ## How extraction breaks cancellation ```kotlin launch { runLoop() } // we cancel this later fun runLoop() { // plain function: no suspend, no CoroutineScope receiver while (true) { /* work */ } // nothing checks cancellation } ``` The helper has no access to the coroutine's `Job`. If someone naively tries to add a check, they may reference an `isActive` that resolves to `true` (no Job) or fall back to `while (true)` — and cancellation silently does nothing. ## Correct extractions **Make it `suspend`** (carries `coroutineContext`): ```kotlin suspend fun runLoop() { while (currentCoroutineContext().isActive) { /* work */ } // or: ensureActive() / yield() } ``` **Give it a `CoroutineScope` receiver:** ```kotlin fun CoroutineScope.runLoop() { while (isActive) { /* work */ } // isActive bound to this scope's Job } // call: launch { runLoop() } ``` **Pass a check function** when neither fits: ```kotlin fun runLoop(isActive: () -> Boolean) { while (isActive()) { /* work */ } } ``` ## Why it matters - Cancellation only works when the check is bound to the **running coroutine's `Job`**. - A `suspend` function automatically captures that via `coroutineContext`; a plain function does not. - Reading `isActive` with no `Job` gives a false `true`, masking the bug at compile time. ## Rule of thumb > Cancellable CPU helpers should be `suspend` (use `currentCoroutineContext().ensureActive()` / `yield()`) or take a `CoroutineScope` receiver — never a plain function relying on an ambient `isActive`.
- Why does Kotlin default isActive to true with no Job?Semantically, no Job means there's nothing that can be cancelled, so the context is treated as active. It's a convenience that unfortunately hides extraction bugs.
- How does making the helper suspend fix it?A suspend function has access to coroutineContext at the call site's coroutine, so currentCoroutineContext()/ensureActive()/yield() see the real Job and its cancellation state.
saying these in an interview costs you the question
- Assuming isActive throws or is false when no Job exists
- Extracting loops into plain functions and expecting cancellation
- Using a fresh CoroutineScope() inside the helper just to read isActive
- Hardcoding while (true) in extracted compute helpers
- Not binding the check to the running coroutine's Job