tokio-rs/tokio · error · io::Error

task was cancelled

Error message

task was cancelled

What it means

From<JoinError> for io::Error maps Repr::Cancelled to the message 'task was cancelled'. It occurs when code converts a JoinError (from a JoinHandle whose task was cancelled) into an io::Error, typically via ? on a JoinHandle in a function returning io::Result.

Source

Thrown at tokio/src/runtime/task/error.rs:173

impl fmt::Debug for JoinError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.repr {
            Repr::Cancelled => write!(fmt, "JoinError::Cancelled({:?})", self.id),
            Repr::Panic(p) => match panic_payload_as_str(p) {
                Some(panic_str) => {
                    write!(fmt, "JoinError::Panic({:?}, {:?}, ...)", self.id, panic_str)
                }
                None => write!(fmt, "JoinError::Panic({:?}, ...)", self.id),
            },
        }
    }
}

impl std::error::Error for JoinError {}

impl From<JoinError> for io::Error {
    fn from(src: JoinError) -> io::Error {
        io::Error::new(
            io::ErrorKind::Other,
            match src.repr {
                Repr::Cancelled => "task was cancelled",
                Repr::Panic(_) => "task panicked",
            },
        )
    }
}

fn panic_payload_as_str(payload: &SyncWrapper<Box<dyn Any + Send>>) -> Option<&str> {
    // Panic payloads are almost always `String` (if invoked with formatting arguments)
    // or `&'static str` (if invoked with a string literal).
    //
    // Non-string panic payloads have niche use-cases,
    // so we don't really need to worry about those.
    if let Some(s) = payload.downcast_ref_sync::<String>() {
        return Some(s);
    }

View on GitHub (pinned to 625954f365)

Solutions

  1. Before awaiting, check handle.is_finished() or match on the JoinError to distinguish cancellation from panic.
  2. Avoid converting JoinError to io::Error wholesale; handle Err(JoinError) explicitly.
  3. Don't abort tasks whose results you still need; use a cancellation token (CancellationToken) for cooperative cancellation.
  4. Catch the cancelled branch in select! instead of awaiting it afterward.

Example fix

// before
let r = handle.await?; // converts JoinError -> io::Error 'task was cancelled'
// after
match handle.await {
    Ok(v) => v,
    Err(e) if e.is_cancelled() => return,
    Err(e) => return Err(e.into()),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before awaiting, check whether the task was aborted.
if handle.is_finished() { /* may be cancelled */ }

Type guard

// Distinguish JoinError variants before converting.
fn is_cancelled(e: &tokio::task::JoinError) -> bool { e.is_cancelled() }

Try / catch

match handle.await {
    Ok(v) => v,
    Err(e) if e.is_cancelled() => {
        // task was cancelled; handle gracefully
        return;
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Awaiting a JoinHandle whose task was aborted via handle.abort() or cancelled during runtime shutdown, then propagating it as io::Error.

Common situations: Calling task.abort() and then awaiting the JoinHandle in a function returning io::Result; runtime shutdown cancelling outstanding tasks; select! that aborts a branch then awaits its handle.

Related errors


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/0270f5466654a459. Report an issue: GitHub.