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…