#290 Jul 30, 2026

290. Mutex::clear_poison — One Panicked Thread Shouldn't Poison the Lock Forever

A worker panics while holding the lock, and from then on every lock() in your program returns Err. If the data is still consistent, you can undo the poisoning instead of apologizing at every call site.

Poisoning is a Mutex feature, not a bug: a panic mid-update might leave the data half-written, so the lock starts refusing service to make you look. But plenty of panics can’t corrupt anything — the classic case is a worker that panicked after its update, or one that never got to write at all:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::sync::Mutex;
use std::thread;

let m = Mutex::new(vec![1, 2, 3]);

thread::scope(|s| {
    let _ = s.spawn(|| {
        let _guard = m.lock().unwrap();
        panic!("worker died");   // guard dropped mid-panic
    }).join();
});

assert!(m.is_poisoned());
assert!(m.lock().is_err());      // ...and now everyone pays

The workaround you usually see is shrugging at every single call site:

1
let data = m.lock().unwrap_or_else(|e| e.into_inner());

That works, but it’s a per-call-site decision repeated forever — and it silently accepts future poisonings too, including the ones that do mean corruption.

Mutex::clear_poison (stable since 1.77) is the one-time version: inspect the data, decide it’s fine, flip the flag back:

1
2
3
4
5
6
7
8
9
{
    // recover once, look at the data, decide
    let data = m.lock().unwrap_or_else(|e| e.into_inner());
    assert_eq!(*data, vec![1, 2, 3]);  // still consistent
}
m.clear_poison();

assert!(!m.is_poisoned());
assert_eq!(m.lock().unwrap().len(), 3);  // Ok again, everywhere

After clear_poison, every lock() in the program returns Ok again — the recovery decision is made once, by the code that actually checked the invariants, not smeared across every caller as a reflexive unwrap_or_else.

The same method exists on RwLock. And if your critical sections genuinely can’t be interrupted mid-invariant — you only ever replace the value wholesale, say — the per-site into_inner() shrug is defensible. But the moment “is the data OK?” has a real answer, clear_poison puts that answer in exactly one place.

← Previous 289. JoinHandle::is_finished — Check on a Worker Without Blocking on join() Next → 291. catch_unwind — One Panicking Job Shouldn't Kill the Whole Worker