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()
orexpect()
onOption
orResult
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.
Related Macros
-
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!
andassert_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.
- Rust walks back up the call stack, calling 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.