#289 Jul 29, 2026

289. JoinHandle::is_finished — Check on a Worker Without Blocking on join()

join() tells you when a thread is done — by blocking until it is. Sometimes you just want to peek: still running, or can I collect the result now?

The DIY version is an Arc<AtomicBool> that the worker sets as its last statement. It has a hole: if the thread panics before that store, the flag stays false forever and your supervisor waits on a corpse.

JoinHandle::is_finished (stable since 1.61) asks the handle directly. It returns immediately, and it reports termination — a worker that panicked counts as finished, because it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::thread;
use std::time::Duration;

let workers: Vec<_> = (0..4)
    .map(|id| thread::spawn(move || work(id)))
    .collect();

// progress report while they run — no blocking
while workers.iter().any(|w| !w.is_finished()) {
    let done = workers
        .iter()
        .filter(|w| w.is_finished())
        .count();
    println!("{done}/4 workers finished");
    thread::sleep(Duration::from_millis(100));
}

is_finished() doesn’t consume the handle, so you can poll in a loop and still join() afterwards. And you should: is_finished() == true is exactly the guarantee that join() won’t block, and join() is where the result — or the panic — comes out:

1
2
3
4
5
6
for w in workers {
    match w.join() {
        Ok(n) => println!("produced {n}"),
        Err(_) => eprintln!("a worker panicked"),
    }
}

That second half matters. is_finished tells you that a thread ended, never how — a clean return and a panic look identical until you join(). Name your threads with thread::Builder and the panic message at least tells you who died.

One boundary to respect: this is a check-in tool, not a synchronization primitive. A hot while !w.is_finished() {} loop is the same spin-wait sin as the atomic-counter hack from this morning’s Barrier bite. If you need to wait for completion, join() already does that; if you need results as they arrive, use a channel. Reach for is_finished when the answer “not yet” is useful — progress bars, health checks, deciding whether to steal a straggler’s remaining work.

← Previous 288. std::sync::Barrier — Make All Threads Start Phase 2 Together Next → 290. Mutex::clear_poison — One Panicked Thread Shouldn't Poison the Lock Forever