tokio-rs/tokio · error · io::Error
task panicked
Error message
task panicked
What it means
From<JoinError> for io::Error maps Repr::Panic(_) to the message 'task panicked'. It surfaces when a spawned task panics and the JoinHandle error is propagated as io::Error (e.g. via ?). The original panic payload is lost in this conversion; only the generic message survives.
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
- Match on the JoinError and call into_panic()/resume_unwind to preserve and propagate the real panic.
- Remove unwrap/expect from spawned tasks; use proper error returns.
- Log the panic source via e.is_panic() before converting.
- Separate task error handling from io::Result flows to avoid the lossy From conversion.
Example fix
// before
let v = handle.await?; // 'task panicked' as io::Error
// after
match handle.await {
Ok(v) => v,
Err(e) if e.is_panic() => std::panic::resume_unwind(e.into_panic()),
Err(e) => return Err(e.into()),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before awaiting, anticipate panic risk: design tasks to return Result<T, E> // rather than panicking.
Type guard
fn is_panic(e: &tokio::task::JoinError) -> bool { e.is_panic() } Try / catch
match handle.await {
Ok(v) => v,
Err(e) if e.is_panic() => {
std::panic::resume_unwind(e.into_panic());
}
Err(e) => return Err(e.into()),
} Prevention
- Resume panics via into_panic()/resume_unwind to preserve the original payload.
- Avoid unwrap/expect inside spawned tasks; return Result.
- Log panic sources before any conversion.
When it happens
Trigger: Awaiting a JoinHandle of a task that panicked, inside a function returning io::Result, so the JoinError converts to io::Error.
Common situations: unwrap()/expect() inside a spawned task; assertion failures; indexing out of bounds; propagating via ? which triggers From<JoinError>.
Related errors
- task was cancelled
- `JoinError` reason is not a panic.
- There must be more than one worker
- Can't get a task id when not inside a task
- cannot create LocalSet during thread shutdown
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/fb7baf571394bafc.
Report an issue: GitHub.