What is invokeOnCancellation for, and what are the rules around resuming after cancellation?
answer
- Hook runs on cancellation to stop underlying work
- Receives the cancellation cause (Throwable?)
- Must be fast, non-blocking, non-throwing
- Resume after cancel is dropped -> use resume onCancellation
- isActive guards a manual resume
basics
~10 sinvokeOnCancellation runs a cleanup block when the coroutine is cancelled while suspended, so you can stop the underlying work. After cancellation any value you try to resume with is ignored, so close it safely.
solid answer
~40 sinvokeOnCancellation on a CancellableContinuation registers a handler that runs when the continuation is cancelled (the surrounding coroutine is cancelled while it is suspended). Its job is to release the resource you started in the bridge: cancel the network Call, unregister the listener, close the stream. The handler receives the cancellation cause (nullable Throwable). Critical rule: a cancelled continuation is already completed, so calling resume afterwards is a no-op or, for a value the caller will never see, may leak the resource — use cont.resume(value) { closeResource() } (the onCancellation parameter of resume) to close a resource that arrives after cancellation. invokeOnCancellation should be fast, non-blocking, and must not throw; exceptions there are treated as uncaught. Register it once, typically right after starting the async work.
go deeper
Knows invokeOnCancellation cleans up when cancelled.
Registers cleanup correctly and knows resume after cancellation is dropped.
Handles the resume/cancel race and uses the resume onCancellation overload to avoid leaks.
Reasons about handler context, non-throwing constraints, and designing leak-free bridges under concurrency.
## Why cancellation needs a hook When you suspend inside `suspendCancellableCoroutine`, the coroutine may be **cancelled** by its parent (timeout, scope cancelled, structured-concurrency failure) *while you are waiting for the callback*. The continuation is then completed with a `CancellationException` and the suspend function throws. But the **underlying async work keeps running** unless you stop it. `invokeOnCancellation` is the hook that stops it. ```kotlin suspendCancellableCoroutine { cont -> val call = api.enqueue(callback) cont.invokeOnCancellation { cause -> call.cancel() // release the resource } } ``` - The lambda receives the **cancellation cause** (`Throwable?`). - It runs **on cancellation only**, not on normal completion. - It should be **fast and non-throwing** — it may run in an undefined context, and a thrown exception is reported as uncaught. ## The resume-after-cancellation race There is an inherent race: the coroutine is cancelled at roughly the same moment the callback fires. Two things to know: 1. **A continuation is resumed at most once and cancellation counts as completion.** If cancellation wins, a later `cont.resume(value)` is silently ignored — `value` is dropped. 2. **A dropped value can leak.** If `value` is a resource (open socket, cursor), it never reaches the caller and never gets closed. To handle this, `resume` has an overload with an `onCancellation` lambda: ```kotlin cont.resume(resource) { _, _, _ -> resource.close() } ``` The lambda runs if the continuation was already cancelled, letting you close the orphaned value. (Older single-arg form: `cont.resume(value) { resource.close() }`.) ## isActive / isCancelled You can also guard manually: `if (cont.isActive) cont.resume(value)`. `isActive` is false once cancelled or resumed. This avoids work but does not by itself close an already-built resource — combine with the onCancellation lambda when the value owns resources. ## Summary of rules - Register `invokeOnCancellation` to stop the underlying work. - Keep it fast, non-blocking, non-throwing. - Resume exactly once; resumes after cancellation are dropped. - Use the `resume(value, onCancellation)` overload to release a value that loses the race.
- A callback delivers an open Cursor right after cancellation; how do you avoid leaking it?Use cont.resume(cursor) { cursor.close() } (resume with onCancellation) so the cursor is closed if the continuation was already cancelled.
- Can invokeOnCancellation throw?It must not; a thrown exception is treated as uncaught and reported through the coroutine exception machinery, so keep it defensive.
saying these in an interview costs you the question
- Thinks invokeOnCancellation also runs on normal completion
- Does blocking I/O or long work inside the handler
- Assumes resume after cancellation throws instead of being ignored
- Ignores resource leaks from dropped resume values
- Lets the handler throw exceptions