#291 Jul 30, 2026

291. catch_unwind — One Panicking Job Shouldn't Kill the Whole Worker

Job 2 panics and your worker thread dies with it — jobs 3 through 400 never run. std::panic::catch_unwind stops the panic at a boundary you choose.

A panic unwinds until something catches it or the thread dies. In a job loop, “the thread dies” means every queued job after the bad one is silently dropped:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn run_job(id: usize) -> usize {
    if id == 2 {
        panic!("job {id} choked");
    }
    id * 10
}

for id in 0..4 {
    run_job(id); // job 2 kills the loop
}

catch_unwind runs a closure and converts any panic inside it into an Err, so the loop survives:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::panic;

for id in 0..4 {
    match panic::catch_unwind(|| run_job(id)) {
        Ok(v) => println!("job {id} -> {v}"),
        Err(payload) => {
            let msg = payload
                .downcast_ref::<String>()
                .map(|s| s.as_str())
                .unwrap_or("unknown panic");
            eprintln!("job {id} failed: {msg}");
        }
    }
}
// job 0 -> 0
// job 1 -> 10
// job 2 failed: job 2 choked
// job 3 -> 30

The Err carries the panic payload as Box<dyn Any + Send>. A panic! with a format string stores a String; a bare string literal stores a &'static str — downcast to whichever you expect (or try both) to recover the message.

Three things to know before you reach for it. It’s a boundary tool — thread pools, FFI edges, plugin callbacks — not try/catch for control flow; fallible code should return Result. It can’t catch anything if the build uses panic = "abort". And it doesn’t undo side effects: a panic while holding a lock still poisons itcatch_unwind decides where unwinding stops, and this morning’s clear_poison handles what it left behind.

If you only wanted to observe the panic and pass it on — log and rethrow — use panic::resume_unwind(payload): it continues unwinding with the original payload and skips printing a second panic message.

← Previous 290. Mutex::clear_poison — One Panicked Thread Shouldn't Poison the Lock Forever Next → 292. panic::set_hook — You Caught the Panic, but It Still Screamed to stderr