We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 3 of 24
JoinHandle, panics and thread::Result
Start one thread per input, let some of them panic, and report what happened to each thread in input order.
pub fn survive_panics(inputs: Vec<i64>) -> Vec<String>
The worker body is already written in the starter code and must stay as it
is. The worker for an input x does one of three things:
| input | worker does | report entry |
|---|---|---|
x > 0 |
returns x * 2 |
"ok:<x * 2>", for example "ok:6" for 3 |
x < 0 |
panic!("bad {x}") |
"err:bad <x>", for example "err:bad -2" for -2 |
x == 0 |
panic!("zero") |
"err:zero" |
So [1, -2, 3] produces ["ok:2", "err:bad -2", "ok:6"].
The contract:
- Return exactly one string per input, at the same position as its input. Threads may finish in any order; the report must still follow input order.
- A panic in one worker must not stop the other workers, and
survive_panicsitself must return normally. - An empty input returns an empty vector.
- Positive inputs are at most
i64::MAX / 2(4611686018427387903), sox * 2cannot overflow. Negative inputs may be anyi64, includingi64::MIN, because a negative input is only formatted, never doubled. - Inputs are short lists (the tests use at most five values), so starting one operating system thread per input is fine here.
- Numbers are written in plain decimal, with a leading
-for negatives and no spaces other than the one afterbad.
What happens when a spawned thread panics
A panic is Rust's way of stopping work after a bug or an unrecoverable
condition. In the default configuration (described at the end), a panic
unwinds: it walks back up that thread's call stack, running destructors
(Drop implementations) for the values it passes. When a panic unwinds out of
a thread started with std::thread::spawn, only that thread ends. Other
threads keep running, and the thread that spawned it is not interrupted.
The value the panic carried, called the payload, is kept for whoever
joins the thread. thread::spawn returns a JoinHandle<T>, and
pub fn join(self) -> std::thread::Result<T>
waits for the thread to finish. std::thread::Result<T> is defined as
type Result<T> = std::result::Result<T, Box<dyn Any + Send + 'static>>;
If the thread returned normally, join gives Ok(value). If it panicked,
join gives Err(payload); it does not panic again in the joining thread.
The error type is a box holding a value of some unknown type, dyn Any. The
type is unknown because a payload can be any Send + 'static value:
std::panic::panic_any(42) panics with an i32 payload, for example. So the
standard library cannot promise a string.
That is why the starter does not compile. It tries to format the payload
with {payload}, which needs the Display trait. Box<T> implements
Display only when T does, and dyn Any + Send does not, so the compiler
reports:
error[E0277]: `dyn Any + Send` doesn't implement `std::fmt::Display`
The compiler also suggests {:?}. That compiles, but Debug for dyn Any
prints Any { .. }, not the panic message.
Getting a message out of the payload
dyn Any has a method downcast_ref::<T>(), which returns Option<&T>:
Some if the boxed value really is a T, None otherwise. You can call it
directly on the box. The question is which T to ask for, and the answer
depends on how the panic was written. With the standard library used here:
| panic | payload type |
|---|---|
panic!("zero"): a plain string literal, nothing to substitute |
&'static str |
panic!("bad {x}"): a message that substitutes a value |
String |
A literal message is already a fixed string, so it is passed along as a
&'static str. A message that inserts x has to be built at run time, so it
becomes a String. The standard library documents that panic! in the 2021
edition or later always produces one of these two types, but not which form
produces which, so check both. Other payload types come from
std::panic::panic_any, from std::panic::resume_unwind (which starts
unwinding with any boxed value you give it), or from panic!(x) with a
non-string x in the 2018 edition and earlier, so keep a fallback for those
too. This exercise uses both panic! forms on purpose: a solution that checks
only one type produces the fallback instead of the message for the other kind
of panic and fails the tests that contain it.
Check &str first and String second (either order works, because a value
has exactly one type), then add a fallback branch for any other payload. The
reference solution reports such a payload as "err:unknown"; none of the
tests produce one, so the fallback text is your choice.
downcast_ref::<&str>() returns Option<&&str>: a reference to the payload,
and the payload is itself a &str. Formatting or calling to_string() on a
&&str works the same as on a &str.
Panic messages on standard error
When panic! runs, it first calls the panic hook, a process-wide function
that reports the panic. The default hook prints a report to standard error
naming the thread ('<unnamed>' for threads created without a name), the
source location that panicked, and the message. The exact layout varies
between Rust versions. So even a correct solution's run prints one such
report per panicking worker, and they can appear in any order. They go to standard
error, not standard output, so they do not change the JSON result the harness
prints.
std::panic::set_hook can replace the hook, but the hook is shared by the
whole process: replacing it changes how every panic in every thread is
reported, including panics in code you do not own. Do not install a hook in
this exercise.
Limits of this technique
Everything above relies on the panic strategy being unwind, which is the
default for most targets. A program built with panic = "abort" in its Cargo
profile (or -C panic=abort) ends the whole process at the first panic in any
thread, so join never gets an Err to inspect. Unwinding also does not
help if a destructor panics while a panic is already unwinding; that aborts
the process too.
Catching panics is a tool for isolation boundaries, such as a worker pool
that should keep serving other jobs when one job hits a bug. It is not the
usual way to report expected failures. If a worker can fail in a way callers
should handle, have it return a Result and join that, so the error has a
real type instead of an Any box.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.