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
- Before awaiting, check handle.is_finished() or match on the JoinError to distinguish cancellation from panic.
- Avoid converting JoinError to io::Error wholesale; handle Err(JoinError) explicitly.
- Don't abort tasks whose results you still need; use a cancellation token (CancellationToken) for cooperative cancellation.
- 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
- Match on JoinError instead of ?-converting to io::Error.
- Use CancellationToken for cooperative cancellation.
- Avoid aborting tasks whose result you still need.
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
- task panicked
- `JoinError` reason is not a panic.
- unexpected xucred version from LOCAL_PEERCRED
- A Tokio 1.x context was found, but it is being shutdown.
- A Tokio 1.x context was found, but it is being shutdown.
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/0270f5466654a459.
Report an issue: GitHub.