join()
JoinHandle::join waits (blocks the calling thread) for the associated thread to finish executing and returns its outcome as a std::thread::Result<T>:
pub type Result<T> = std::result::Result<T, Box<dyn Any + Send + 'static>>;
impl<T> JoinHandle<T> {
pub fn join(self) -> Result<T>;
}
Ok(value): The spawned thread returnedvalue: Tnormally.Err(panic_payload): The spawned thread panicked while executing; the panic is caught at the thread boundary and returned asBox<dyn Any + Send + 'static>.
Why join(self) Consumes the JoinHandle<T> by Value
join takes self by value (fn join(self) -> Result<T>) for two reasons:
- It moves the non-
Copyreturn valueTout of the thread packet to the caller without cloning. - Consuming
JoinHandle<T>makes it a compile-time error to call.join()twice on the same thread!
Anti-Pattern: Calling .join() Inside the Spawning Loop
A frequent beginner mistake is chaining .join().unwrap() directly onto thread::spawn(...) inside a for loop. Doing so blocks the parent thread until Thread 0 finishes before Thread 1 is even spawned—serializing execution completely!
To run $N$ threads in parallel, collect all JoinHandle<T> instances into a Vec first, and then call .join() in a second loop:
Anti-Pattern (Serialized — 1 Thread Runs at a Time):
for item in items {
thread::spawn(move || work(item)).join().unwrap(); // Waits before spawning next!
}
Idiomatic Pattern (Parallel — All N Threads Run Simultaneously):
let handles: Vec<_> = items.into_iter().map(|i| thread::spawn(move || work(i))).collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
use std::thread;
fn compute_square(n: u64) -> u64 {
n * n
}
fn main() {
let inputs = [2_u64, 4, 6, 8];
// Phase 1: Spawn all worker threads so they execute concurrently:
let handles: Vec<thread::JoinHandle<u64>> = inputs
.into_iter()
.map(|n| thread::spawn(move || compute_square(n)))
.collect();
// Phase 2: Join all handles in order to collect their return values:
let squares: Vec<u64> = handles
.into_iter()
.map(|handle| handle.join().expect("worker thread panicked"))
.collect();
println!("inputs: {:?}", inputs);
println!("squares: {:?}", squares);
}
inputs: [2, 4, 6, 8]
squares: [4, 16, 36, 64]
Propagating a Worker Panic on .join()
When a worker thread panics, calling handle.join().unwrap() panics the parent thread with debug output, whereas calling std::panic::resume_unwind(err) on the Err(err) payload re-raises the original panic payload cleanly on the parent thread:
use std::panic;
use std::thread;
fn main() {
let handle = thread::spawn(|| {
panic!("worker encountered fatal invariant failure");
});
match handle.join() {
Ok(()) => println!("worker succeeded"),
Err(payload) => {
if let Some(msg) = payload.downcast_ref::<&'static str>() {
println!("caught worker panic in join(): {:?}", msg);
}
// If you want the parent thread to propagate the exact panic:
let caught = panic::catch_unwind(panic::AssertUnwindSafe(|| {
panic::resume_unwind(payload);
}));
println!("resumed unwind caught: {}", caught.is_err());
}
}
}
caught worker panic in join(): "worker encountered fatal invariant failure"
resumed unwind caught: true