Error Handling
panic! for bugs, Result for failures, and the ? operator — Rust's two-channel error design.
Part of Rust Interview Mastery · Based on The Rust Programming Language
Unrecoverable Errors
When should code panic! versus return a Result? Give the design rule.
panic! = a bug or violated contract — array out of bounds, an "impossible" state, broken internal invariant. It means someone wrote wrong code, not something went wrong in the world.
Result = expected failure — file not found, network timeout, bad user input. Callers should decide what to do.
The heuristic from the book: if a reasonable caller would want to recover, return Result. Panic is for situations where continuing would be unsafe or meaningless. Libraries panic sparingly; applications panic at boundaries.
What actually happens when a thread panics — unwinding vs abort?
By default Rust unwinds: it walks the stack backwards running destructors (Drop) for every live value in each frame — memory is still freed properly. Then the thread dies; other threads keep running (a poisoned Mutex is the wrinkle, ch 15).
You can set panic = "abort" in Cargo.toml's [profile.release] — the process dies immediately, no cleanup, smaller binary, no catching. OS reclaims everything anyway. Also RUST_BACKTRACE=1 gives the stack trace that makes panics debuggable.
unwrap vs expect — is there a real difference?
Mechanically nearly identical — both extract Ok/Some or panic. The difference is communication: expect("config file must exist") lets you attach why this failure is impossible/fatal, which shows up in the panic message.
Interview answer: unwrap() says "trust me"; expect("reason") says "here's the invariant." Code review treats bare unwraps on anything caller-controlled as a smell; expect is fine for startup invariants like "database URL must parse."
Result, Propagation & the ? Operator
Show error propagation with ? — what exactly does the operator do?
fn read_username(path: &str) -> Result<String, io::Error> {
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
Ok(s)
}
expr? on Result<T, E>: if Ok(t) → unwraps to t and continues; if Err(e) → returns Err(e) from the enclosing function, converting via From to the function's error type. On Option it short-circuits to None.
It compresses "try, and if failed bubble up" into one character — which is exactly why Rust didn't need exceptions.
Can main return a Result? What does that enable?
Yes — fn main() -> Result<(), Box<dyn Error>> is legal. That makes ? usable directly in main, and returning Err prints the Debug error and exits non-zero:
fn main() -> Result<(), Box<dyn Error>> {
let config = fs::read_to_string("app.toml")?;
let pool = DbPool::connect(&config)?;
Ok(())
}
Box<dyn Error> is the trait-object escape hatch for "any error type" — great for binaries/prototypes. Libraries define concrete error enums instead. (Production tip: anyhow/thiserror formalize these two modes.)
Where does the conversion in ? come from — how do different error types mix?
When ? early-returns an Err(e), it calls From::from(e) to convert to the function's declared error type. So fn f() -> Result<(), MyError> can ? an io::Error iff impl From<io::Error> for MyError exists.
This is why custom error enums derive From impls per upstream error, or why Box<dyn Error> (which has a blanket From<E: Error>) absorbs anything. Without a conversion you get "the trait From<io::Error> is not implemented" — the compiler making error-type plumbing explicit.
What are the unwrap_or family of methods good for?
let v = res.unwrap_or(0); // cheap default let v = res.unwrap_or_else(|_| 42); // lazy default (runs only on Err) let v = res.map_or(0, |x| x * 2); // default + transform in one let v = res.unwrap_or_default(); // T::Default::default()
unwrap_or_else vs unwrap_or is the classic detail: the argument to unwrap_or is evaluated eagerly — if the default is expensive (DB query, allocation), lazy unwrap_or_else only pays when you actually need it.
When is it actually OK to unwrap in production code?
Three defensible cases:
- Invariant-by-construction:
"127.0.0.1".parse::<IpAddr>().unwrap()— the literal can't fail; document withexpect. - Tests and prototypes — panics ARE the failure signal.
- Fatal startup errors — config unreadable, port taken: crashing loudly at boot beats limping.
The book's nuance: expect with a message about your invariant, and put it in the commit message / comment. What you don't do: unwrap on data that crossed a network, file, or user boundary.