Skip to main content

Cancellation

In Rust async:

Cancellation means dropping a future before it completes.

That’s it.
There is no “cancel token” built into the language.

If a future is:

  • Dropped → it is cancelled
  • Never polled again
  • Its destructor (Drop) runs

Where cancellation comes from

Common sources:

  • Dropping a JoinHandle
  • A select! branch winning
  • A timeout expiring
  • Shutdown signals
  • Parent task exiting
let handle = tokio::spawn(do_work());
drop(handle); // task cancelled

Cancellation is cooperative

Important rule:

Async Rust cancellation is cooperative, not forced.

That means:

  • The runtime does not stop your code mid-instruction
  • Cancellation only happens at .await points
  • Your code must be written to be safely droppable

Cancellation safety (critical concept)

A future is cancellation-safe if:

Dropping it at any .await does not leave the program in a broken state.

Not cancellation-safe

async fn write_two_steps(socket: &mut TcpStream) -> io::Result<()> {
socket.write_all(b"HELLO").await?;
socket.write_all(b"WORLD").await?;
Ok(())
}

If cancelled after writing HELLO:

  • Protocol is broken
  • Peer sees partial message

Cancellation-safe version

async fn write_message(socket: &mut TcpStream) -> io::Result<()> {
let msg = b"HELLOWORLD";
socket.write_all(msg).await?;
Ok(())
}

One await → atomic at protocol level.

Destructors (Drop) run on cancellation

This is huge.

struct Guard;

impl Drop for Guard {
fn drop(&mut self) {
println!("cleaning up");
}
}

async fn task() {
let _guard = Guard;
tokio::time::sleep(Duration::from_secs(10)).await;
}

If task is cancelled:

  • _guard is dropped
  • Cleanup runs

This is your main tool for cleanup.

Timeouts are just cancellation

Timeouts are implemented by cancelling the future.

use tokio::time::{timeout, Duration};

let result = timeout(Duration::from_secs(1), async {
do_work().await
}).await;

If time expires:

  • The inner future is dropped
  • timeout returns Err(Elapsed)

Handling timeouts cleanly

match timeout(Duration::from_secs(1), do_work()).await {
Ok(Ok(value)) => println!("success: {value}"),
Ok(Err(e)) => println!("task error: {e}"),
Err(_) => println!("timed out"),
}

Nested Results:

  • Inner → task error
  • Outer → timeout error

select! = manual cancellation control

select! races futures and cancels losers.

use tokio::select;

async fn slow() {
tokio::time::sleep(Duration::from_secs(5)).await;
println!("slow done");
}

async fn fast() {
tokio::time::sleep(Duration::from_secs(1)).await;
println!("fast done");
}

#[tokio::main]
async fn main() {
select! {
_ = slow() => {}
_ = fast() => {}
}
}

When fast completes:

  • slow is dropped (cancelled)

Graceful cancellation with signals

Using a cancellation channel

use tokio::sync::watch;

async fn worker(mut shutdown: watch::Receiver<bool>) {
loop {
tokio::select! {
_ = shutdown.changed() => {
println!("shutting down");
break;
}
_ = do_work() => {}
}
}
}

This allows:

  • Explicit shutdown
  • Cleanup paths

CancellationToken (Tokio utility)

use tokio_util::sync::CancellationToken;

let token = CancellationToken::new();
let child = token.child_token();

tokio::spawn(async move {
tokio::select! {
_ = child.cancelled() => println!("cancelled"),
_ = do_work() => {}
}
});

token.cancel();
  • Structured cancellation
  • Tree-based propagation

Blocking and cancellation

Blocking code

std::thread::sleep(Duration::from_secs(10));
  • Cannot be cancelled
  • Freezes executor thread

Async sleep

tokio::time::sleep(Duration::from_secs(10)).await;
  • Cancellation-safe
  • Droppable

Cancellation and streams

Streams are cancelled by:

  • Dropping the stream
  • Exiting loop early
while let Some(item) = stream.next().await {
if item == 42 {
break; // stream cancelled
}
}

Best practices

  • Minimize .await in critical sections
  • Use RAII for cleanup
  • Make protocol steps atomic
  • Prefer select! over flags
  • Assume cancellation can happen anytime

Mental model to remember forever

Cancellation = dropping a future at an .await.

Or:

“If it can be awaited, it can be cancelled.”