#294 Aug 1, 2026

294. AssertUnwindSafe — catch_unwind Won't Touch Your &mut Until You Sign the Waiver

You wrapped the job loop in catch_unwind (bite 291), added a completed-counter — and the build broke: the type `&mut i32` may not be safely transferred across an unwind boundary. AssertUnwindSafe is how you tell the compiler you’ve thought it through.

catch_unwind requires its closure to be UnwindSafe — a marker trait, like Send, that the compiler derives automatically. Capturing a &mut breaks it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::panic;

let mut completed = 0;
for id in 0..4 {
    let result = panic::catch_unwind(|| {
        let v = run_job(id);
        completed += 1; // error[E0277]: the type
        // `&mut i32` may not be safely trans-
        // ferred across an unwind boundary
        v
    });
}

The concern is logical corruption, not memory safety: if the closure panics halfway through updating something it borrowed, you catch the panic and then keep using data that may be half-updated. Types with poisoning like Mutex (bite 290) handle this themselves and stay UnwindSafe; a raw &mut T or RefCell<T> can’t make that promise, so catch_unwind refuses them.

Here the fix is a judgment call, and it’s an easy one — completed += 1 is the last statement, so a panic can’t leave it torn. Wrap the closure in AssertUnwindSafe to vouch for it:

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

let mut completed = 0;
let mut failed = 0;
for id in 0..4 {
    let result = panic::catch_unwind(
        AssertUnwindSafe(|| {
            let v = run_job(id);
            completed += 1;
            v
        }),
    );
    if result.is_err() {
        failed += 1;
    }
}
assert_eq!(completed, 3);
assert_eq!(failed, 1);

AssertUnwindSafe(x) is a zero-cost wrapper that implements UnwindSafe unconditionally — no unsafe, no runtime check. You can’t cause undefined behavior with it; the worst case is observing state a panic left half-updated, which is exactly what you’re asserting can’t happen (or doesn’t matter).

Rule of thumb: on Err, either discard the state the closure touched or make sure every mutation is panic-proof — then AssertUnwindSafe is a fact, not a wish.

← Previous 293. thread::panicking — A Second Panic in Drop Doesn't Unwind, It Aborts Next → 295. String::leak — A &'static str From Runtime Data, Without the Box Detour