Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🦀 Tip of the day 35 follow-up

Follow-up questions from the discussion on Tip 35: Panicking, Drop Everything and Run.

In unwind mode with catch_unwind, is panic! basically an exception, just without any data about the cause?

Half right. In unwind mode a panic does walk the stack and run destructors, and catch_unwind lets you intercept it at a boundary, so the mechanism rhymes with exceptions. But the “no data about the cause” part is wrong: a panic carries a payload.

When you call panic!, the payload is the message you passed:

#![allow(unused)]
fn main() {
panic!("file not found: {path}");        // payload is a String
std::panic::panic_any(MyError { code }); // payload is your own type
}

panic_any (stable since Rust 1.51) lets you panic with a value of any 'static + Any + Send type, not just a string. That is how you attach structured data to a panic.

On the catching side, catch_unwind returns std::thread::Result<R>, which is Result<R, Box<dyn Any + Send + 'static>>. The Err holds the payload, type-erased. You recover the original type with downcast_ref:

#![allow(unused)]
fn main() {
use std::panic;

let result = panic::catch_unwind(|| {
    panic!("boom");
});

if let Err(payload) = result {
    if let Some(s) = payload.downcast_ref::<String>() {
        eprintln!("panicked with: {s}");
    } else if let Some(e) = payload.downcast_ref::<MyError>() {
        eprintln!("panicked with error code {}", e.code);
    }
}
}

One catch: the payload type for a plain panic! is not guaranteed. A formatted message like panic!("file: {path}") lands as a String, while a bare literal like panic!("boom") lands as a &'static str. The standard library documents this distinction as unspecified and free to change between versions, so do not lean on a specific type when downcasting a generic panic. If you want a payload you can reliably recover, use panic_any with your own type.

So yes, the payload exists, but treating it like an exception’s getCause() is the wrong instinct. catch_unwind is a boundary tool: FFI edges (unwinding across the C/Rust boundary is undefined behavior), test runners that keep going after one test fails, and thread isolation. It is not a control-flow mechanism. The moment you reach for it to model expected failures, you want Result instead.

🔇 The payload is right there, type-erased and waiting. You just have to guess what it is before you are allowed to look at it.