15.2 Unrecoverable Errors in Rust

Typical unrecoverable errors in Rust include:

  • Out-of-bounds access of vectors, arrays, or slices
  • Division by zero
  • Invalid UTF-8 in string conversions
  • Integer overflow in debug mode
  • Use of unwrap() or expect() on Option or Result types containing no data

These cause an automatic call to the panic! macro, resulting in program termination.

15.2.1 The panic! Macro and Implicit Panics

For handling unrecoverable error conditions, Rust provides the panic! macro, which terminates the current thread and begins unwinding the stack, cleaning up resources.

Example:

fn main() {
    panic!("Critical error occurred!");
}

This produces an error message and backtrace, aiding in debugging. The output includes valuable information such as the file name, line number, and a stack trace pointing to where the panic occurred.

However, panics in Rust are not limited to explicit use of the panic! macro. Certain operations, such as accessing an array with an invalid index, will also trigger a panic automatically, ensuring that unsafe or unexpected behavior does not go unnoticed.

  • assert! Macro:

    Checks that a condition is true, panicking if it is not.

    fn main() {
        let number = 5;
        assert!(number == 5); // Passes
        assert!(number == 6); // Panics with message: "assertion failed: number == 6"
    }
  • assert_eq! and assert_ne! Macros:

    Compare two values for equality or inequality, panicking with a detailed message if the assertion fails.

    fn main() {
        let a = 10;
        let b = 20;
        assert_eq!(a, b); // Panics with message showing both values
    }

These macros and the panic! macro are typically used to ensure invariants during program execution or in example code or for testing purposes.

15.2.2 Catching Panics

In other languages like Java or Python, exceptions can be caught and handled to prevent the program from terminating abruptly. Rust, being a systems language with a focus on safety, does not use exceptions in the same way. However, it is possible to catch panics in Rust using the std::panic::catch_unwind function.

Example:

use std::panic;
fn main() {
    let i: usize = 3 * 3; // might be optimized out, resulting in an immediate compile time index error
    let result = panic::catch_unwind(|| {
        let array = [1, 2, 3];
        println!("{}", array[i]); // This will panic
    });
    match result {
        Ok(_) => println!("Code executed successfully."),
        Err(err) => println!("Caught a panic: {:?}", err),
    }
}

Output:

Caught a panic: Any

Important Notes:

  • Limited Use Cases: Catching panics is generally discouraged and should be used sparingly, such as in test harnesses or when embedding Rust in other languages.
  • Not for Normal Control Flow: Panics are intended for unrecoverable errors, and relying on catch_unwind for regular error handling is not idiomatic Rust.
  • Performance Overhead: There is some overhead associated with unwinding the stack, so catching panics can impact performance.

15.2.3 Customizing Panic Behavior

Rust allows you to customize panic behavior:

  • Panic Strategy in Cargo.toml:

    [profile.release]
    panic = "abort"
    
    • unwind (default): Performs stack unwinding, calling destructors and cleaning up resources.
    • abort: Terminates the program immediately without unwinding the stack.
  • Environment Variables for Backtraces:

    RUST_BACKTRACE=1 cargo run
    

    This provides a backtrace when a panic occurs, useful for debugging.

15.2.4 Stack Unwinding vs. Aborting

When a panic occurs with the default unwind strategy:

  • Stack Unwinding:
    • Rust walks back up the call stack, calling destructors (drop methods) for all in-scope variables.
    • Resource Cleanup: Ensures that resources like files and network connections are properly closed.
    • Memory Management: Memory allocated on the heap is properly deallocated through destructors.

When the panic strategy is set to abort:

  • Immediate Termination:
    • The program terminates immediately without unwinding the stack.
    • Destructors are not called, so resources may not be cleaned up properly.
  • Resource Leaks:
    • Open files, network connections, and other resources that rely on destructors for cleanup may not be closed.
    • However, the operating system reclaims memory and releases resources associated with the process upon termination.
  • Use Cases:
    • abort may be preferred in environments where binary size and startup time are critical, or where you cannot unwind the stack (e.g., in some embedded systems).

Drawbacks of Using abort:

  • Resource Cleanup: Without stack unwinding, destructors are not called, potentially leading to resource leaks.
  • State Corruption: External systems relying on graceful shutdown or cleanup may be left in an inconsistent state.
  • Debugging Difficulty: Lack of backtraces and cleanup may make debugging more challenging.

Considerations:

  • Safety vs. Performance: While abort can improve performance and reduce binary size, it sacrifices the safety guarantees provided by stack unwinding.
  • Default Behavior: The default unwind strategy is recommended unless you have specific reasons to change it.