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:

  1. It walks back up the call stack.
  2. 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.
  3. 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 if condition is false.
  • assert_eq!(left, right): Panics if left != right, showing the differing values.
  • assert_ne!(left, right): Panics if left == 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 (or full) enables printing a stack trace showing the function call sequence leading to the panic!.
    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.