15.5 Unrecoverable Errors and panic!
While Result
is the standard for handling expected failures, Rust uses panic!
for situations deemed unrecoverable, typically indicating a bug.
15.5.1 The panic!
Macro
Invoking panic!("Error message")
causes the current thread to stop execution abruptly. By default, Rust performs stack unwinding:
- It walks back up the call stack.
- For each stack frame, it runs the destructors (
drop
implementations) of all live objects created within that frame, cleaning up resources like memory and file handles. - After unwinding completes, the thread terminates. If it’s the main thread, the program exits with a non-zero status code, usually printing the panic message and potentially a backtrace.
fn main() { // This code will panic and, by default, unwind the stack before terminating. panic!("A critical invariant was violated!"); }
Some language constructs can also trigger implicit panics, turning potential undefined behavior (common in C/C++) into deterministic crashes:
- Array Index Out of Bounds: Accessing
my_array[invalid_index]
. - Integer Overflow: In debug builds, arithmetic operations like
+
,-
,*
panic on overflow. (In release builds, they typically wrap, similar to C). - Assertion Failures: Using macros like
assert!
,assert_eq!
,assert_ne!
.
Consider array bounds checking. In C, accessing an array out of bounds leads to undefined behavior. Rust prevents this with bounds checks:
fn main() { let data = [10, 20, 30]; // Attempting to access an out-of-bounds index: let element = data[5]; // Index 5 is out of bounds for length 3 println!("Element: {}", element); // This line will not be reached }
Important Note on Compile-Time vs. Runtime Checks: In the specific example above using the constant index 5
, the Rust compiler is often able to detect the out-of-bounds access at compile time due to optimizations and built-in lints (like unconditional_panic
), issuing a compile-time error.
However, the crucial point is that Rust performs these bounds checks at runtime whenever the index cannot be proven safe or unsafe at compile time (e.g., if the index comes from user input, function arguments, or complex calculations). If such a runtime bounds check fails, the program will panic, preventing the memory safety violations common in C/C++. The example data[5]
serves to illustrate this fundamental safety guarantee (bounds check leading to termination instead of UB), even though this specific literal case might be caught earlier by the compiler.
15.5.2 Assertion Macros
Assertions declare conditions that must be true at a certain point in the program. If the condition is false, the assertion macro calls panic!
. They are primarily used to enforce internal invariants and in tests.
assert!(condition)
: Panics ifcondition
isfalse
.assert_eq!(left, right)
: Panics ifleft != right
, showing the differing values.assert_ne!(left, right)
: Panics ifleft == right
, showing the equal values.
fn check_positive(n: i32) { assert!(n > 0, "Input number must be positive, got {}", n); println!("Number {} is positive.", n); } fn main() { check_positive(10); check_positive(-5); // This call will panic }
15.5.3 When to Panic vs. Return Result
The choice between panic!
and Result
is fundamental to Rust error handling:
Use panic!
when:
- A bug is detected (e.g., violated invariant, impossible state reached). The program is in a state you didn’t anticipate and cannot safely handle.
- An operation is fundamentally unsafe to continue (e.g., index out of bounds prevents memory safety).
- In examples, tests, or prototypes where you need to signal failure immediately without complex error handling.
Use Result
when:
- The error represents an expected or potential failure condition (e.g., file not found, network unavailable, invalid input).
- The caller might be able to recover or react meaningfully to the error (e.g., retry, prompt user, use default).
- You are writing library code. Libraries should generally avoid panicking, allowing the calling application to decide the error handling strategy.
Overusing panic!
makes code less resilient and harder for others to integrate. Reserve it for truly exceptional, unrecoverable situations that indicate a programming error.
15.5.4 Customizing Panic Behavior
- Abort on Panic: Instead of unwinding (which has some code size overhead), you can configure Rust to immediately abort the entire process upon panic. This yields smaller binaries but skips destructor cleanup. Configure this in
Cargo.toml
:[profile.release] panic = "abort"
- Backtraces: For debugging panics, environment variable
RUST_BACKTRACE=1
(orfull
) enables printing a stack trace showing the function call sequence leading to thepanic!
.RUST_BACKTRACE=1 cargo run
15.5.5 Catching Panics (catch_unwind
)
Rust provides std::panic::catch_unwind
to execute a closure and catch any panic that occurs within it. If the closure completes successfully, catch_unwind
returns Ok(value)
. If the closure panics, it returns Err(panic_payload)
, where the payload contains information about the panic.
use std::panic; fn panicky_function(trigger_panic: bool) { println!("Function start."); if trigger_panic { panic!("Intentional panic triggered!"); } println!("Function end (no panic)."); } fn main() { println!("Catching potential panic..."); let result = panic::catch_unwind(|| { panicky_function(true); // This call will panic }); match result { Ok(_) => println!("Call completed normally."), Err(payload) => println!("Caught panic! Payload: {:?}", payload), } println!("Execution continues after catch_unwind."); println!("\nRunning without panic..."); let result_ok = panic::catch_unwind(|| { panicky_function(false); // This call will succeed }); match result_ok { Ok(_) => println!("Call completed normally."), Err(payload) => println!("Caught panic! Payload: {:?}", payload),//Not reached } }
Use catch_unwind
with extreme caution. It is not intended for general error handling (use Result
for that). Legitimate uses include:
- Testing Frameworks: Isolating tests so a panic in one test doesn’t crash the whole suite.
- Foreign Function Interface (FFI): Preventing Rust panics from unwinding across language boundaries (e.g., into C code), which is undefined behavior.
- Thread Management: Allowing a controlling thread to detect and potentially restart a worker thread that panicked.
Do not use catch_unwind
to simulate exception handling for recoverable errors.