iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

panic! vs Result

Rust panics: unrecoverable errors, catch_unwind, panic_handler, and the design choice between panic and Result.

Rust — panics

EXAMPLE
// ===== When panics happen =====
// - Explicit: panic!("message")
// - Array out-of-bounds: v[100] on a Vec of length 3
// - Integer overflow in debug mode (release wraps)
// - Unwrap / expect on None or Err
// - assert! / assert_eq!

fn main() {
    let v = vec![1, 2, 3];
    let _ = v[100];     // panic: index out of bounds
}

// ===== panic! macro =====
panic!("something went wrong");
panic!("got {} expected {}", actual, expected);

// ===== unreachable! / unimplemented! / todo! =====
fn classify(n: i32) -> &'static str {
    match n.cmp(&0) {
        std::cmp::Ordering::Less => "negative",
        std::cmp::Ordering::Equal => "zero",
        std::cmp::Ordering::Greater => "positive",
    }
}

fn parse(s: &str) -> i32 {
    todo!("implement parsing")    // compiles, panics at runtime if reached
}

// ===== What panic does =====
// Default: unwinds the stack, running destructors, then aborts the thread.
// In a single-threaded program, that ends the program.
// In a multi-threaded program, only the thread panics; others continue (with join() returning err).

// ===== Configure abort vs unwind =====
// Cargo.toml
[profile.release]
panic = "abort"       // smaller binaries, no unwinding

// abort is faster + smaller; unwind allows recovery via catch_unwind.

// ===== catch_unwind (rare; for FFI boundaries) =====
use std::panic;

let result = panic::catch_unwind(|| {
    do_risky_thing()
});
match result {
    Ok(v) => println!("ok: {:?}", v),
    Err(_payload) => println!("panicked"),
}

// Use catch_unwind to:
// - Prevent a panic from crossing FFI boundary (C calling Rust)
// - Isolate untrusted plugin code

// ===== When to use panic vs Result =====
// Panic: programmer errors (invariants violated, impossible states)
//        Example: an internal index that 'cannot' be wrong
// Result: expected failures the caller should handle
//        Example: file not found, network timeout, parse failure
//
// Library APIs almost always return Result.
// Application 'main' may unwrap Result if an early panic with a clean message is fine.

// ===== Better panics for libraries =====
let user = users.get(&id).expect("user should exist; populated at startup");
// expect() reads better than unwrap() because it explains the assumption.

// ===== Customising panic output =====
// std::panic::set_hook(Box::new(|info| {
//     eprintln!("custom panic handler: {info}");
// }));

// ===== no_std panic_handler =====
#[no_std]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}
// Embedded / kernel code must provide a panic_handler.

// ===== Patterns to internalise =====
// - Result for recoverable errors; panic for invariant violations
// - expect() with a clear message > unwrap()
// - panic = abort in release for smaller binaries
// - catch_unwind only at FFI / plugin boundaries

// ===== Pitfalls =====
// - unwrap() everywhere -> reintroduces 'NullPointerException' style crashes
// - Catching panics as a general error-handling pattern (use Result)
// - Different panic strategy across crates causing build oddities
// - Skipping integer overflow checking in debug because release wraps silently

Why it matters

Panics are unrecoverable by design. Reach for them on invariant violations; reach for Result on expected failures. expect() with a message explains the assumption; catch_unwind belongs at FFI boundaries. Most Rust code never explicitly panics — the type system catches the cases that would.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Recoverable: Result + ?
// Unrecoverable: panic!("impossible state")
let v: Vec<i32> = vec![];
// v[0]  // panics: index out of bounds
Try it Yourself »

Discussion

Loading…