Rust Interview Mastery · Chapter 09 09

Error Handling

panic! for bugs, Result for failures, and the ? operator — Rust's two-channel error design.

8 questions 2 topics Intermediate

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:

  1. Invariant-by-construction: "127.0.0.1".parse::<IpAddr>().unwrap() — the literal can't fail; document with expect.
  2. Tests and prototypes — panics ARE the failure signal.
  3. 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.