# Cancel safety in Rust's Future

Jeongyun Moon, Donghyun Koh · 2026-05-23

Rust’s async ecosystem is built around `Future`, and asynchronous programming has become increasingly common in servers, networking, databases, and many other systems applications. However, unlike threads or async features from other languages, Rust futures may be cancelled. This means async program may need to be written carefully, since cancellation can interrupt operations.

## Future cancellation can be the problem

```rust
use std::{collections::BTreeSet, sync::Arc, time::Duration};

use axum::{
    extract::{Json, Path, State},
    http::StatusCode,
    routing::get,
    Router,
};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use uuid::Uuid;

const MAX_FILES: usize = 2;

#[derive(Clone)]
struct AppState {
    files: Arc<Mutex<BTreeSet<Uuid>>>,
}

#[derive(Deserialize)]
struct SaveRequest {
    text: String,
}

#[derive(Serialize)]
struct SaveResponse {
    id: String,
}

async fn save(
    State(state): State<AppState>,
    Json(req): Json<SaveRequest>,
) -> Result<Json<SaveResponse>, StatusCode> {
    let id = Uuid::new_v4();
    let filename = id.to_string();

    let mut files = state.files.lock().await;

    if files.len() >= MAX_FILES {
        return Err(StatusCode::INTERNAL_SERVER_ERROR);
    }

    if let Err(err) = tokio::fs::write(&filename, req.text).await {
        let _ = tokio::fs::remove_file(&filename).await;
        eprintln!("write failed: {err}");

        return Err(StatusCode::INTERNAL_SERVER_ERROR);
    }

    // Simulate lagging
    tokio::time::sleep(Duration::from_secs(1)).await;

    files.insert(id);

    Ok(Json(SaveResponse {
        id: id.to_string(),
    }))
}

// ...
```

This web server provides create/get/delete operations for text resources under a quota limit of 2 files.

In the normal execution path of the `save` function, a new text file is created for the generated ID, and the file ID is inserted into the in-memory set. However, if the HTTP request is cancelled during the `sleep` — for example due to a connection error or client disconnect — execution of the `save` future is cancelled immediately, while the file itself remains on disk. As a result, orphaned files are created without being tracked by the server state. This allows users to occupy resources beyond the intended quota limit.

This demonstrates an important property of async Rust: if a cancelable `Future` is written without considering cancellation behavior, the program can easily become inconsistent.

## Why is Future cancellable?

```rust
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub struct Context<'a> { /* private fields */ }

impl<'a> Context<'a> {
    pub const fn waker(&self) -> &'a Waker { ... }
}

pub struct Waker { /* private fields */ }

impl Waker {
    pub fn wake(self) { ... }
    pub fn wake_by_ref(&self) { ... }
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}
```

The core components of Rust’s `Future` trait are `poll` and `wake`.

To execute a `Future`, a runtime such as **Tokio** calls its `poll` method. Depending on the state of the operation, `poll` returns either `Poll::Ready` or `Poll::Pending`. When `Poll::Pending` is returned, the `Future` implementation is responsible for calling `wake` when it may be able to make progress again. After being woken, the runtime calls `poll` again. This process repeats until the `Future` returns `Poll::Ready`.

Unlike an ordinary function, a `Future` is not guaranteed to run to completion unless the runtime keeps polling it. Even with a proper runtime, execution is usually guaranteed in normal cases, but there are some exceptions where a `Future` may be stopped before completion and dropped.

Therefore, when writing code that uses `Future`s, you must either ensure that the `Future` is not cancelled, or make it safe to cancel. This is especially easy to miss with `async fn`, because it looks like an ordinary function. To behave like an ordinary(blocking) function, the `Future` produced by the async function must not be dropped before completion.

## Common sources of cancellation

### Tokio macros

- `tokio::select!` : It returns the output of the first future that completes and drops the remaining futures.
- `tokio::try_join!`: It drops the remaining futures when one future returns `Err`.

### Handlers

When a `Future` is passed to an external function, that may be dropped depending on the implementation. This is especially important for server handlers, as some server libraries drop the handler `Future` when the client connection is closed.

Test results (2s sleep in handler future):

| Framework | Version | Whether request future dropped on disconnection |
| --- | --- | --- |
| Axum | 0.8.9 | Dropped |
| Salvo | 0.93.0 | Dropped |
| Warp | 0.4.3 | Dropped |
| Tonic | 0.14.6 (gRPC) | Dropped |
| Actix Web | 4.13.0 | Not dropped |
| Rocket | 0.5.1 | Not dropped |
| Poem | 3.1.12 | Not dropped |

## How to make Future cancel-safe

### `tokio::spawn`

The `JoinHandle` returned by `tokio::spawn` is cancel-safe. Dropping the `JoinHandle` does not stop the spawned task.

```rust
async fn save(
    State(state): State<AppState>,
    Json(req): Json<SaveRequest>,
) -> Result<Json<SaveResponse>, StatusCode> {
    tokio::spawn(async move {
        ...
    })
    .await
    .unwrap_or_else(|_| Err(StatusCode::INTERNAL_SERVER_ERROR))
}
```

For `save` function example, you can rewrite it like this.

### `CancellationToken`

```rust
async fn some_handler() -> Result<&'static str, StatusCode> {
    let cancellation_token = CancellationToken::new();
    let _drop_guard = cancellation_token.clone().drop_guard();
    tokio::spawn(async move {
        for i in 0..5 {
            if cancellation_token.is_cancelled() {
                break;
            }
            println!(">ㅅ< {i}");
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        }
        Ok("Hello")
    })
    .await
    .unwrap_or_else(|_| Err(StatusCode::INTERNAL_SERVER_ERROR))
}
```

If cancellation is required, `tokio-util`’s `CancellationToken` and `DropGuard` can be used to gracefully stop a spawned task when the outer future is dropped.

### Channels

Async methods of `tokio` channels such as `tokio::sync::oneshot` and `tokio::sync::mpsc` are cancel-safe.

## How to make the program safe

All futures in the program should satisfy at least one of the following:

1.  Ensure the future is never dropped.
2.  Make the future cancel-safe.

For example, the `main` future automatically satisfies the first condition, so cancellation usually does not need to be considered there. For other futures, first option is usually better if it is possible.

For a future to be cancel-safe, every future it awaits must also be cancel-safe, which is difficult to guarantee. In complex logic, wrapping work in `spawn` may be the only practical solution, but spawning every function is inefficient.

A good rule of thumb is to allow cancellation only at a few well-defined boundaries, and keep the internal logic non-cancelled whenever possible.

### Caution

Avoiding direct cancellation of Futures is generally preferred, but cancelling the job itself is still an important problem to handle. If cancellation is not handled properly, a program may continue wasting resources on work that is no longer needed. Additionally, if a spawned task returns a `Result`, its output must be handled. Otherwise, errors may become silent and go unnoticed. This is because Rust does not “throw” exceptions. In languages like JavaScript that do, an unhandled exception in asynchronous code becomes “Uncaught Error”.

## Better design?

```rust
trait CancelSafeFuture: Future {}
```

One possible alternative design would be marker traits for cancellation-safe futures, where combinators such as `select!` only accept those futures. However, unlike traits such as `Send` or `Sync`, cancellation-safety would likely be impossible to infer automatically.

## Future in Other languages

In some languages, users cannot control the execution loop directly, so future-like operations are never cancelled. In **JavaScript**, the user does not control the event loop. Once a **callback** is registered, it will be executed when the operation completes. In **Go**, the user does not control the scheduler. A **goroutine** will resume when its asynchronous operation completes. There is `tokio::select!` -like operation in Go but only channel operation(send / receive) can be selected. Go also does not distinguish async and sync code at the language level.