tokio-rs/tokio · error

`JoinError` reason is not a panic.

Error message

`JoinError` reason is not a panic.

What it means

JoinError::into_panic calls try_into_panic().expect('JoinError reason is not a panic.'). It panics (double panic risk) when called on a JoinError that is NOT a panic — i.e. it represents a cancelled task. into_panic assumes the underlying task panicked.

Source

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

    /// ```should_panic,ignore-wasm
    /// use std::panic;
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     let err = tokio::spawn(async {
    ///         panic!("boom");
    ///     }).await.unwrap_err();
    ///
    ///     if err.is_panic() {
    ///         // Resume the panic on the main task
    ///         panic::resume_unwind(err.into_panic());
    ///     }
    /// }
    /// ```
    #[track_caller]
    pub fn into_panic(self) -> Box<dyn Any + Send + 'static> {
        self.try_into_panic()
            .expect("`JoinError` reason is not a panic.")
    }

    /// Consumes the join error, returning the object with which the task
    /// panicked if the task terminated due to a panic. Otherwise, `self` is
    /// returned.
    ///
    /// # Examples
    ///
    /// ```should_panic,ignore-wasm
    /// use std::panic;
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     let err = tokio::spawn(async {
    ///         panic!("boom");
    ///     }).await.unwrap_err();
    ///
    ///     if let Ok(reason) = err.try_into_panic() {

View on GitHub (pinned to 625954f365)

Solutions

  1. Check e.is_panic() before calling e.into_panic().
  2. Prefer try_into_panic() which returns Result and won't panic.
  3. Match on the error kind (cancelled vs panic) explicitly.
  4. Treat cancellation separately from panic recovery in your error path.

Example fix

// before
let panic_payload = err.into_panic(); // panics if cancelled
// after
if err.is_panic() {
    std::panic::resume_unwind(err.into_panic());
} else {
    // cancelled
    return;
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Guard with is_panic before calling into_panic:
if err.is_panic() { /* safe to into_panic */ }

Type guard

fn into_panic_safe(e: tokio::task::JoinError) -> Result<Box<dyn std::any::Any + Send>, tokio::task::JoinError> {
    e.try_into_panic().map_err(|e| e)
}

Try / catch

match err.try_into_panic() {
    Ok(payload) => std::panic::resume_unwind(payload),
    Err(e) => { /* cancelled */ return; }
}

Prevention

When it happens

Trigger: Calling err.into_panic() on a JoinError that is actually Repr::Cancelled (task was aborted), not Repr::Panic.

Common situations: Unconditionally calling into_panic() after handle.await.err() without checking is_panic(); confusing cancellation with panic in error handling.

Related errors


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