Thread Panics
In Rust, panics are isolated per thread. When a spawned worker thread panics (under the default panic = "unwind" strategy), the Rust runtime unwinds only that thread's stack, runs the Drop destructors of all initialized local variables on that thread, catches the unwind at the top of the thread entry point, and stores the panic payload inside the thread's JoinHandle<T> without crashing other running threads.
Thread Panic Lifecycle
Worker Thread Stack Parent Thread
+-----------------------------------+ +-----------------------------------+
| 1. `panic!("failed to parse")` | | (Continues running normally!) |
| 2. Unwinds worker stack frames, | | |
| running `Drop` on worker locals| | |
| 3. Caught at thread boundary | | |
| 4. Stores `Err(Box<dyn Any + Send>)` ---> | `match handle.join()` |
+-----------------------------------+ | `Err(payload)` -> Inspect or |
| `resume_unwind(payload)` |
+-----------------------------------+
| Scenario | What Happens When a Thread Panics |
|---|---|
| Worker thread panics (later joined) | Unwinds worker stack, prints panic hook message to stderr, and returns Err(payload) from handle.join() |
Worker thread panics (detached / JoinHandle dropped) |
Unwinds worker stack and prints panic hook message; other threads continue running (unless a shared Mutex is poisoned and .unwrap()ed by another thread) |
Scoped worker thread panics (thread::scope) |
thread::scope waits for all sibling threads to finish, and then automatically propagates the panic if the caller did not manually .join() and handle the panicking ScopedJoinHandle! |
| Main thread panics | Unwinds the main thread stack and exits the process with exit code 101 |
Extracting the Panic Message from JoinHandle::join()
The Err variant of std::thread::Result<T> has type Box<dyn Any + Send + 'static>. Because panic!("literal") constructs a &'static str payload and panic!("formatted {}", x) constructs a String payload, downcasting to both &'static str and String recovers any standard panic message:
use std::any::Any;
use std::thread;
fn extract_panic_message(payload: Box<dyn Any + Send + 'static>) -> String {
if let Some(&s) = payload.downcast_ref::<&'static str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
String::from("non-string panic payload")
}
}
fn main() {
let h1 = thread::spawn(|| {
panic!("static literal panic");
});
let h2 = thread::spawn(|| {
let code = 503;
panic!("formatted panic with code {code}");
});
for (idx, handle) in [h1, h2].into_iter().enumerate() {
if let Err(payload) = handle.join() {
println!("thread #{idx} panicked with: {:?}", extract_panic_message(payload));
}
}
}
thread #0 panicked with: "static literal panic"
thread #1 panicked with: "formatted panic with code 503"
Customizing Panic Reporting with std::panic::set_hook
Before stack unwinding begins on any thread, the Rust runtime invokes the global panic hook. Calling std::panic::set_hook(Box::new(|panic_info| ...)) at application startup allows logging structured JSON crash reports (including thread::current().name() and panic_info.location()) to your telemetry system whenever any thread panics.