scoped cancellation and propagation
You send three runners to fetch a price from three shops, intending to use whichever answers first. As soon as one returns, the other two errands are pointless — you want to call them back, not let them keep running and waste effort. Scoped cancellation is the machinery for exactly that: a way to signal a task, and all the tasks nested under it, to stop early and unwind cleanly. Propagation is the part that makes the signal travel down to descendants and travel up as a result.
Cancellation in modern async is almost always cooperative, not forceful. You do not violently kill a task mid-instruction — that would leak locks and half-written state. Instead a cancellation request sets a flag (a cancellation token, or in Rust simply dropping the future). The task notices at its next yield point: each await also checks 'have I been cancelled?', and if so it stops, runs its cleanup (closing files, releasing locks, the destructors / Drop), and unwinds. Scoped means the signal follows structure: cancelling a task scope cancels every child spawned inside it, recursively, so you cancel a subtree with one request. Propagation flows two ways: a cancel request flows down to descendants; and a failure or a timeout in one task can flow up to its scope, which then cancels the siblings — the 'first one wins, cancel the rest' pattern.
This matters because without it, concurrent programs leak runaway work: timeouts that fire but leave the slow operation still churning, a failed request that abandons its half-finished helpers. Scoped cancellation ties a task's lifetime to a deadline, a parent, or a 'whichever finishes first' race, and guarantees the losers are wound down. Two honest points: because it is cooperative, a task that never reaches a yield point (a tight CPU loop, or blocking code) cannot be cancelled — it must reach an await to notice. And cancellation must run cleanup correctly, or you trade a leaked task for a leaked file handle or a held lock; cancellation-safety is a real design concern, not free.
Race with a timeout (Rust/Tokio): tokio::select! { r = work() => r, _ = sleep(timeout) => give_up() } // when the timer wins, work()'s future is dropped — cancellation runs its cleanup at the next await.
Dropping the losing future cancels it cooperatively; it stops at its next yield point and unwinds cleanly.
Cooperative cancellation cannot interrupt a task that never yields — a blocking call or CPU loop ignores the signal. And a cancelled task must still run its cleanup; sloppy cancellation leaks locks and file handles. 'Cancellation-safe' is a property you design for, not a default.