RustArena

Step 6 of 7

When a thread panics

A panic in a spawned thread unwinds that thread only. The rest of the program keeps running, and the panic shows up when you join: join() returns Err instead of Ok. The usual thread '<unnamed>' panicked at ... message is printed to stderr as always.

use std::thread;

fn average(scores: Vec<u32>) -> u32 {
    scores.iter().sum::<u32>() / scores.len() as u32
}

fn main() {
    let teams = vec![vec![80, 90], vec![], vec![70, 75, 80]];
    let mut handles = Vec::new();
    for scores in teams {
        handles.push(thread::spawn(move || average(scores)));
    }
    for (i, handle) in handles.into_iter().enumerate() {
        match handle.join() {
            Ok(avg) => println!("team {i}: average {avg}"),
            Err(_) => println!("team {i}: worker panicked"),
        }
    }
}
team 0: average 85
team 1: worker panicked
team 2: average 75

Team 1 has no scores, so average divides by zero and its thread panics. The other two workers and main carry on.

What's inside the Err

join() returns Result<T, Box<dyn Any + Send>>. The error is the panic payload: whatever was passed to panic!, boxed as a dyn Any (module 25). You can look inside with downcast_ref: a panic!("literal") carries a &str, a panic!("{}", x) with formatting carries a String.

use std::thread;

fn main() {
    let handle = thread::spawn(|| -> u32 {
        panic!("sensor 7 sent garbage");
    });
    let err = handle.join().unwrap_err();
    if let Some(msg) = err.downcast_ref::<&str>() {
        println!("worker failed: {msg}");
    }
}
worker failed: sensor 7 sent garbage

Scoped threads

If a scoped thread panics and you don't join it yourself, thread::scope still waits for every other thread to finish, and then panics with a scoped thread panicked:

use std::thread;

fn main() {
    let names = vec!["ana", "bo"];
    thread::scope(|s| {
        s.spawn(|| names[5].len());
    });
    println!("never printed");
}

Panics are for bugs

Remember module 19: panic! means a bug, Result means an expected failure. Bad input from a user or a file is expected, so a worker should return a Result (a JoinHandle<Result<i32, String>>) and main handles Err like any other value. Catching panics through join is for code you don't control, and for keeping a long-running program alive when one worker hits a bug (a web server doesn't stop because one request handler crashed).

Common mistakes

Your task

Each worker parses one line of a sensor log with .parse::<i32>().unwrap(), so a bad line makes that worker panic. The threads are already spawned. Join them in order and print line N: VALUE for a good line or line N: bad input for a worker that panicked (N counts from 1). Finally print how many lines were good:

line 1: 21
line 2: 19
line 3: bad input
line 4: 23
line 5: bad input
good lines: 3

Challenge 2

Now do it the better way: the workers must not panic. Each one returns a Result<i32, String>, turning the parse error into its message with .map_err(|e| e.to_string()). Join in order and print:

line 1: 21
line 2: 19
line 3: error: invalid digit found in string
line 4: 23
line 5: error: invalid digit found in string

Challenge 3

risky_parse comes from another team's crate: it panics on bad input, and you can't change it. Write fn parse_all(inputs: Vec<String>) -> Vec<Option<u8>> that calls risky_parse in one thread per input and returns Some(value) for each worker that succeeded and None for each one that panicked, in input order.

Previous: Send and Sync: what may cross threads Next: rayon: data parallelism made easy