In Rust async, dropping a Future cancels it. This can cause issues:
rustasync fn dangerous() { let mut data = fetch_initial().await; // If cancelled HERE, we have partial state! modify(&mut data); save(data).await; // Never runs if cancelled above }
Cancellation-Safe Operations
An operation is cancellation-safe if cancelling and restarting produces correct results.
Safe:
tokio::time::sleep()- Can restart from beginningTcpStream::read()- Data stays in bufferchannel::recv()- Message not lost on cancel
Unsafe:
BufReader::read_line()- Partial data lost!- Custom operations with intermediate state
Making Operations Safe
Pattern 1: Use cancelation-safe alternatives
rust// Instead of read_line (unsafe) let line = reader.read_line(&mut buf).await?; // Use tokio's cancellation-safe version let line = tokio::io::AsyncBufReadExt::read_line(&mut reader, &mut buf).await?;
Pattern 2: Complete sub-operations atomically
rustasync fn safe_update(db: &Database, id: u64) { // Fetch and modify in one go before any await let update = prepare_update(id); // Now safe to await db.execute(update).await?; }
Pattern 3: Use tokio::select! carefully
rustlet msg = loop { select! { // recv() is cancellation-safe msg = rx.recv() => break msg, _ = timeout => return Err("timeout"), } };
Code Examples
rust
// Cancellation-safe message processing
use tokio::sync::mpsc;
async fn process_messages(mut rx: mpsc::Receiver<Message>) {
loop {
// This is safe: recv() doesn't lose messages on cancel
tokio::select! {
Some(msg) = rx.recv() => {
// Process completely before next select
handle_message(msg).await;
}
_ = tokio::signal::ctrl_c() => break,
}
}
}