Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 3 of 24

Medium Primitives

JoinHandle, panics and thread::Result

Run one thread per input and report what happened to each, in input order.

pub fn survive_panics(inputs: Vec<i64>) -> Vec<String>

Each worker takes its x and:

  • x > 0 — returns x * 2, reported as "ok:<n>";
  • x < 0panic!("bad {x}"), reported as "err:bad -2";
  • x == 0panic!("zero"), reported as "err:zero".

So [1, -2, 3] produces ["ok:2", "err:bad -2", "ok:6"]. A panicking worker must not stop the others, and must not stop the function.

What a panic in a thread actually does

Most languages pick one of two behaviours: the panic tears down the process, or it vanishes silently. Rust does neither. A panic unwinds that thread only, the thread dies, and the panic value is stored in the handle. Every other thread carries on. main is unaffected until it joins.

You collect it here:

pub fn join(self) -> std::thread::Result<T>

which is

type Result<T> = std::result::Result<T, Box<dyn Any + Send + 'static>>;

That error type is the awkward part, and it is honest about the situation: a panic payload can be any value (panic_any will take an arbitrary type), so std cannot promise you a string. It hands you a box and lets you ask.

Which is why the starter does not compile:

error[E0277]: `Box<dyn Any + Send>` doesn't implement `std::fmt::Display`

You cannot print it. You must downcast it.

Downcast order, and why this problem has two panic forms

Box<dyn Any> gives you downcast_ref::<T>() -> Option<&T>: a checked question, “is the value inside actually a T?”. The catch is knowing which T to ask for, and the answer depends on how the panic was written:

panic payload type
panic!("zero") — a literal, no formatting &'static str
panic!("bad {x}") — formatted String

A single format argument changes the type of the payload. This is not a detail you can guess, and it is exactly why the two cases exist in this problem: a solution that handles only one of them passes half the tests. Ask for &str first, then String, and keep a fallback arm for anything else — panic_any(42) is legal and someone’s dependency will do it eventually.

Note downcast_ref::<&str>() gives you an Option<&&str>. A double reference, because the payload is a &str and you asked for a reference to it. That is correct, not a mistake.

Expect noise on stderr

The default panic hook prints thread '<unnamed>' panicked at ... to stderr for every panic, so a successful run of this problem is loud. The grader reads stdout and ignores stderr, so that is cosmetic. In real code you would reach for std::panic::set_hook to quieten expected panics — but a hook is a process-global, so installing one from a library function is rude, and you should not do it here.

The honest caveat

All of this depends on panic = "unwind", which is the default. A build configured with panic = "abort" kills the process on the first panic and there is no Err to inspect, because there is no unwinding. Recovering from panics is a property of the build profile, not a guarantee of the language.

And to be clear about design: this is not how you handle expected failure in Rust. Expected failure is Result. Catching panics is for supervision boundaries — a thread pool that must not die because one job was buggy. If you find yourself panicking in order to join it back, use Result instead.

Loading visualization…