Asynchronous Rust

Cancellation Safety

In Rust, cancelling an asynchronous operation is performed by dropping its Future. Whenever a future is dropped (for example, when a timeout expires or another branch of a select! macro finishes first), execution of that future stops at whichever .await point it was suspended at, and the compiler runs Drop on all local variables currently alive inside that state-machine variant.

Cancellation safety is the property of an async function or method determining whether dropping its future mid-execution inside a loop (select!) loses partially read data or leaves external state inconsistent.

How Dropping a Future Cancels It at an .await Point

async fn process_order() {
    let _guard = ScopeCleanup("order-tx");
    step_1().await;   // <--- Suspended here (`Poll::Pending`) when timeout fires!
    step_2().await;   // <--- Never reached!
}

When Timeout Drops `process_order()` Future:
  1. `step_1()` child future is dropped first.
  2. `_guard` (`ScopeCleanup("order-tx")`) runs its `Drop::drop` destructor!
  3. `step_2()` never executes!
use std::future::pending;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

struct CleanupOnCancel(&'static str);

impl Drop for CleanupOnCancel {
    fn drop(&mut self) {
        println!("  [Drop] cleaned up resource {:?}", self.0);
    }
}

async fn long_running_operation() {
    let _resource = CleanupOnCancel("db-transaction-lock");
    println!("operation started; now awaiting external signal...");
    pending::<()>().await;
    println!("unreachable after pending");
}

fn main() {
    {
        let mut fut = pin!(long_running_operation());
        let mut cx = Context::from_waker(Waker::noop());

        // Poll once: starts operation and suspends at `pending().await`:
        let poll_res = fut.as_mut().poll(&mut cx);
        println!("poll result: {:?} -> now cancelling by dropping `fut`!", poll_res);
    } // `fut` is dropped here, running `CleanupOnCancel::drop` deterministically!

    println!("main continues after cancellation");
}
operation started; now awaiting external signal...
poll result: Pending -> now cancelling by dropping `fut`!
  [Drop] cleaned up resource "db-transaction-lock"
main continues after cancellation

Cancellation-Safe vs. Non-Cancellation-Safe Operations

When using a future repeatedly inside a loop { select! { ... } }, dropping an unselected branch mid-execution only causes data loss if the future had already consumed partial data into its own local variables before hitting a second .await:

Operation Pattern Cancellation-Safe in select!? Reason & How to Fix
rx.recv().await (Channel receive) Yes Single atomic poll: either pops 1 message (Poll::Ready) or leaves queue untouched (Poll::Pending)
stream.read(&mut buf).await (Single syscall read) Yes Reads available bytes in a single poll without internal .await buffering
reader.read_exact(&mut buf).await No! Loops across multiple .await reads; if cancelled after reading 4 of 8 bytes, those 4 bytes are dropped with the future!
Multi-step async fn (let msg = rx.recv().await; send(msg).await;) No! If cancelled while waiting on send(msg).await, msg was already popped from rx and is dropped!

How to Make a Multi-Step Future Cancellation-Safe in a Loop

Instead of re-creating a multi-step future on every iteration of loop { select! { _ = multi_step() => ... } }, create and pin the future once outside the loop (let mut op = pin!(multi_step());) and pass &mut op to select!. That way, when another branch wins an iteration, op is not dropped—it retains its in-progress state for the next loop iteration!