Avoiding usage of unwrap() with JoinHandle

A pattern of creating a vec of std::thread::JoinHandles, adding each thread's handle to it during a for loop creation of threads and then join()ing them at the end with another for loop seems to be quite common, as seen in this stackoverflow question and this example from tiny-http.

   use std::thread;
    
    fn main() {
        let mut handles = vec![];
    
        for _ in 0..4 {
            let handle = thread::spawn(move || {
               todo!();
              //DO THINGS
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap();
        }
    
        println!("Done");
    }

All the examples I have found use unwrap() on the handle.join() call but I would like to avoid panicing in this function (the example above shows calling in the main function but my actual use case is within a non-main function). I am using anyhow and ? to deal with returning errors from functions but I get errors about unsized types when attempting to use it with std::thread::Result<T>.

Looking for any suggestions on handling the result of std::thread::join() without using unwrap(), or reasons why it is not possible.

Thanks

Note: I did find std::thread::scope in one of the suggested threads while writing this post and may go that way, but I am still interested in learning if handing the result of std::thread::join() is possible without using unwrap().

A JoinHandle only returns an error if the thread panics. So, you should generally not try to process the error any other way than panicking (or exiting), unless you are in a situation where if there wasn't a thread involved, you'd choose to use catch_unwind() there to catch the panic unwind. If you wouldn’t choose to catch_unwind(), then don't try to handle that error; just panic.

That said, if you do want to, the thing you need to know about panic payloads is that while they can be values of arbitrary type in the Box<dyn Any>, they are normally going to be either a String or &'static str, so what you can do is try downcasting to those two types and use that string as the error details.

3 Likes

JoinHandle::join returns a Result, so you can collect them into a vec and examine them instead of panicking immediately on error:

use std::thread;

fn main() {
    let mut handles = vec![];

    for i in 0..4 {
        let handle = thread::spawn(move || {
            // Only half the threads panic, so we can demonstrate the difference
            // between success and failure
            if i % 2 == 0 {
                todo!();
            }
        });
        handles.push(handle);
    }

    let errors: Vec<_> = handles
        .into_iter()
        .filter_map(|handle| {
            match handle.join() {
                Ok(()) => None,
                Err(e) => Some(e),
            }
        })
        .collect();
    println!("errors: {errors:#?}");
}
1 Like

This is reinforced in the doc here:

Interesting, the downcasting seems to be a relatively clean solution. Not ideal but good enough.

There is a specific example of matching on the results of join() on the page of std::thread::Result<T> but to be fair, they do use std::panic::resume_unwind() to deal with the error branch.

Where do you see the note suggesting to not handling the error case of std::thread::join()? I don't see anything like that on the page you linked. They give 2 suggested options, one is propagate the panic, the other is handle the panic.

I would still have to downcast here though, if I wanted to have a not Any type to pass to anyhow! macro?

Propagating is “not handling”.

The difference between using resume_unwind() and using .unwrap() is mainly that the first one doesn't call the panic hook again, so it won't print two panic messages. Which is nice, but not fundamental; the result is still "the program prints some debug messages and exits".

I am aware of the distinction.

I am referring to this language from the docs page:

or in case the thread is intended to be a subsystem boundary that is supposed to isolate system-level failures, match on the Err variant and handle the panic in an appropriate way

That is what I am referring to by handling the panic.

You would. That's functionally the only thing you can do with an Any value, and you're forced to deal with Any by the design of the JoinHandle::join method.

In practice, the only thing you're expected to do with a panicking thread is either log the panic reason and abandon the thread, or propagate the panic (using unwrap() or similar). I might quibble about immediately panicking when joining multiple threads, as I might prefer to join all of the threads first and then panicking if one of the threads had panicked, but that's down in the weeds.

unwrap() isn't always a code smell. Panicking on join handles and mutex locks is pretty normal in Rust programs that deal with those things, because the only errors they can return indicate that your program has already panicked somewhere and may no longer be upholding application invariants as a result.

1 Like

I only quoted part of what @kpreid said, he mentioned both options that are described in the doc I linked:

The point is to do whatever you think is best when the code in that thread panics. If that code were not in a thread, and it panicked, it is very reasonable to allow the panic (not catch it). In that case, you should panic when join returns an error.

But in some special cases you might choose to catch the panic. As the doc I linked to explains, this might be the case when you are using the thread to isolate failures. For example, web servers may catch panics that occur in one thread handling a request, to allow the server to continue processing other requests in other threads.

1 Like

Thank you. I read your response too quickly and missed that.