Threads

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 returned value: T normally.
  • Err(panic_payload): The spawned thread panicked while executing; the panic is caught at the thread boundary and returned as Box<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:

  1. It moves the non-Copy return value T out of the thread packet to the caller without cloning.
  2. 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