How does a suspending function like delay() detect cancellation, and what does 'cancellation is checked on resume' mean mechanically?
answer
- suspendCancellableCoroutine + CancellableContinuation
- continuation registered with the Job
- resumed-to-throw, not polled
- invokeOnCancellation for cleanup
- blocking calls aren't suspension points
basics
~10 sCancellable 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.
solid answer
~40 sCancellable suspending functions are built on suspendCancellableCoroutine, which gives a CancellableContinuation. That continuation registers a cancellation handler with the current Job. When cancel() runs, the Job invokes those handlers: the pending operation (e.g. delay's timer) is disposed and the continuation is resumed with a CancellationException. So 'checked on resume' means the framework doesn't poll a flag in your code — instead the suspended coroutine is woken up specifically to throw. delay(), yield(), and well-behaved library suspend functions all do this. A suspend function that ignores its CancellableContinuation's cancellation (or blocks a thread instead of suspending) won't be cancellable. This is why wrapping blocking callbacks needs suspendCancellableCoroutine and an invokeOnCancellation handler, not plain suspendCoroutine.
code
kotlin · 5 linessuspend fun fetch(api: AsyncApi): Data =
suspendCancellableCoroutine { cont ->
val handle = api.start { data -> cont.resume(data) }
cont.invokeOnCancellation { handle.abort() } // dispose on cancel
}go deeper
Recognizes that delay() can be cancelled but Thread.sleep cannot, even without the internals.
Explains suspendCancellableCoroutine, the continuation linked to the Job, and 'resumed to throw'.
Can correctly wrap a blocking/callback API with invokeOnCancellation cleanup and reason about leaks.
Discusses designing cancellable APIs across module boundaries and the contract well-behaved suspend functions must honor.
## What 'suspension point' really is under the hood A `suspend` function compiles to a state machine. At each suspension point the coroutine can hand control back to its dispatcher and store a **continuation** (the object that knows how to resume). The standard way to make a cancellable suspension is `suspendCancellableCoroutine`: ```kotlin suspend fun awaitValue(source: Callback): String = suspendCancellableCoroutine { cont -> val listener = source.onResult { value -> cont.resume(value) } cont.invokeOnCancellation { source.removeListener(listener) } // cleanup on cancel } ``` The `cont` here is a `CancellableContinuation`. It is **linked to the current Job**. ## How cancellation reaches it 1. You call `job.cancel()`. The `Job` moves to `Cancelling`. 2. The `Job` notifies its children and any registered cancellation handlers, including the `invokeOnCancellation { ... }` you registered. 3. The suspended `CancellableContinuation` is **resumed with a `CancellationException`** rather than with a normal value. 4. Your coroutine, which was paused at that point, throws on resume and starts unwinding. So **'cancellation is checked on resume'** does not mean your code polls a boolean. It means: when a coroutine is suspended and the Job is cancelled, the runtime resumes that exact suspended coroutine **for the purpose of throwing**. Functions like `delay()` and `yield()` are implemented exactly this way — `delay()` schedules a timer via `suspendCancellableCoroutine` and disposes it on cancellation. ## Why some suspend functions are NOT cancellable - If you use plain `suspendCoroutine` (no 'Cancellable'), the continuation is not tied to cancellation; the coroutine won't wake up to throw. - If a function **blocks the thread** (e.g. `Thread.sleep`, a blocking JDBC call) instead of suspending, there is no continuation to resume — it is a non-suspending gap and cancellation can't interrupt it. ## Practical consequence To make a third-party blocking/callback API cooperatively cancellable, wrap it with `suspendCancellableCoroutine` and use `invokeOnCancellation` to release resources (cancel the request, remove the listener). That is the bridge between Kotlin's cooperative model and the outside world. ## Note on CancellationException type The thrown type is `CancellationException` (or a subtype like `TimeoutCancellationException`). Catching it broadly with a generic `catch (e: Exception)` and swallowing it breaks cooperative cancellation — see the dedicated question on that pitfall.
- Why won't Thread.sleep(1000) inside a coroutine respond to cancellation?It blocks the thread instead of suspending, so there is no cancellable continuation to resume; use delay() instead, which suspends.
- What is invokeOnCancellation used for?To run cleanup (close resources, cancel the underlying request) when the continuation is cancelled, so wrapping blocking/callback APIs doesn't leak.
saying these in an interview costs you the question
- Thinks delay() spins polling a boolean flag
- Uses suspendCoroutine instead of suspendCancellableCoroutine for cancellable waits
- Believes Thread.sleep is a valid cancellation-aware pause
- Doesn't release resources via invokeOnCancellation when wrapping callbacks