Skip to content

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_exact may not be?
  • How do idempotency keys help after uncertain cancellation?
  • How can a future preserve partial progress across select! iterations?

References

  • Tokio Documentation
  • Async Book