Theme
Q070. What does cancellation safety mean?
Short Answer
An async operation is cancellation-safe when dropping it before completion does not lose logical progress or leave state unusable, so recreating or abandoning it has defined behavior. This matters in select!, timeouts, and task cancellation because any pending branch may be dropped. Cancellation safety does not mean no side effects occurred; it means interruption happens at a documented, recoverable boundary.
Deep Dive
A useful test is: if this future is dropped while pending and later retried, can data be lost, duplicated, or a protocol be corrupted?
Receiving the next item from many queues is cancellation-safe because no item is removed until the future returns it. A read_exact-style operation can be unsafe in a loop if it consumes some bytes internally and then gets dropped; recreating it may restart with the remaining bytes and lose framing context.
Ways to design for cancellation include:
- keep partial progress in an owner outside the temporary future;
- expose one atomic logical step per await;
- make operations idempotent or attach request identifiers;
- pin and retain a long-lived future across
select!iterations; - document points after which retry has different semantics.
Internal Model
Dropping a future destroys its in-memory state. Progress already moved into an external object, kernel buffer, peer, database, or channel is not reversed.
Cancellation safety is therefore an API semantic property, not a marker trait checked by the compiler.
Example
rust
use std::time::Duration;
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (sender, mut receiver) = mpsc::channel(1);
tokio::select! {
message = receiver.recv() => panic!("unexpected message: {message:?}"),
_ = tokio::time::sleep(Duration::from_millis(1)) => {}
}
sender.send(42_u64).await.unwrap();
assert_eq!(receiver.recv().await, Some(42));
}Common Mistakes
- Defining cancellation-safe as "the operation had no side effects."
- Retrying every interrupted operation without idempotency analysis.
- Assuming Rust's type system automatically verifies cancellation safety.
Follow-ups
- Why is queue receive often cancellation-safe while
read_exactmay not be? - How do idempotency keys help after uncertain cancellation?
- How can a future preserve partial progress across
select!iterations?
Related
- Q068. How does
tokio::select!work, and what happens to losing branches? - Q069. How are futures and tasks cancelled in Rust?
- Q074. How do you implement graceful shutdown and task supervision?
References
- Tokio Documentation
- Async Book