Panic and Unwrap
Panic and Unwrap in Rust
Understanding when to panic and how to use unwrap safely is crucial for writing robust Rust code.
Understanding Panic
Panic is Rust's mechanism for handling unrecoverable errors. When a panic occurs, the program prints an error message, unwinds the stack, and exits.
When Panic Occurs
fn main() {
// Explicit panic
panic!("Something went terribly wrong!");
// This line never executes
println!("This won't print");
}
Common Panic Scenarios
fn main() {
// Array index out of bounds
let v = vec![1, 2, 3];
// v[10]; // Panic!
// Integer overflow in debug mode
let x: u8 = 255;
// let y = x + 1; // Panic in debug mode!
// Unwrap on None/Err
let x: Option<i32> = None;
// x.unwrap(); // Panic!
}
The unwrap() Method
unwrap()
extracts the value from Option
or Result
, panicking if it's None
or Err
.
Option::unwrap()
fn main() {
// Safe unwrap - we know it's Some
let x = Some(5);
let value = x.unwrap();
println!("Value: {}", value);
// Dangerous unwrap
let y: Option<i32> = None;
// let value = y.unwrap(); // Panic: "called Option::unwrap() on a None value"
}
Result::unwrap()
use std::fs::File;
fn main() {
// This might panic
// let file = File::open("nonexistent.txt").unwrap();
// Safe when you're certain
let result: Result<i32, &str> = Ok(42);
let value = result.unwrap(); // Safe because we know it's Ok
}
expect() - Unwrap with Message
expect()
is like unwrap()
but with a custom panic message:
use std::fs::File;
fn main() {
// More informative panic message
let file = File::open("config.txt")
.expect("Config file should be present");
let age: Option<u8> = None;
// let age = age.expect("Age must be provided"); // Better error message
}
When to Use Panic
1. Prototyping and Examples
fn main() {
// Quick prototype - error handling can come later
let contents = std::fs::read_to_string("data.txt").unwrap();
let number: i32 = contents.trim().parse().unwrap();
println!("Number: {}", number);
}
2. Tests
#[cfg(test)]
mod tests {
#[test]
fn test_parsing() {
let result = "42".parse::<i32>().unwrap(); // OK in tests
assert_eq!(result, 42);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_division_panic() {
divide(10, 0); // Should panic
}
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("divide by zero");
}
a / b
}
}
3. Impossible Situations
fn process_validated_input(input: &str) {
// We've already validated this can't fail
let number: u32 = input.parse()
.expect("Input was pre-validated");
// Or use unreachable!
match number {
0..=100 => println!("In range"),
_ => unreachable!("Number was validated to be 0-100"),
}
}
Safe Alternatives to unwrap()
unwrap_or()
fn main() {
let x: Option<i32> = None;
let value = x.unwrap_or(0); // Default value instead of panic
println!("Value: {}", value); // 0
let result: Result<i32, &str> = Err("error");
let value = result.unwrap_or(-1);
println!("Value: {}", value); // -1
}
unwrap_or_else()
fn main() {
let x: Option<i32> = None;
let value = x.unwrap_or_else(|| {
println!("Computing default value");
42
});
let path = "data.txt";
let contents = std::fs::read_to_string(path)
.unwrap_or_else(|e| {
eprintln!("Failed to read {}: {}", path, e);
String::from("default content")
});
}
unwrap_or_default()
fn main() {
let x: Option<String> = None;
let value = x.unwrap_or_default(); // Empty string
let numbers: Option<Vec<i32>> = None;
let value = numbers.unwrap_or_default(); // Empty vector
}
Pattern Matching Instead of Unwrap
Match Expression
fn safe_divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Attempt to divide by zero");
}
a / b
}
fn better_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
match better_divide(10.0, 2.0) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero"),
}
}
if let
fn main() {
let config: Option<String> = load_config();
if let Some(cfg) = config {
println!("Config: {}", cfg);
} else {
println!("Using default configuration");
}
}
fn load_config() -> Option<String> {
// Simulated config loading
None
}
Panic Safety
catch_unwind
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
println!("About to panic");
panic!("Oops!");
});
match result {
Ok(_) => println!("No panic occurred"),
Err(_) => println!("Caught a panic!"),
}
println!("Program continues");
}
set_hook
use std::panic;
fn main() {
panic::set_hook(Box::new(|info| {
eprintln!("Custom panic handler: {}", info);
}));
// This will use our custom handler
// panic!("Test panic");
}
Debug vs Release Behavior
fn main() {
// Integer overflow
let x: u8 = 255;
// Debug mode: panic
// Release mode: wrapping
// let y = x + 1;
// Explicit wrapping
let y = x.wrapping_add(1);
println!("y = {}", y); // 0
// Checked arithmetic
match x.checked_add(1) {
Some(result) => println!("Result: {}", result),
None => println!("Overflow would occur"),
}
}
Best Practices
1. Use expect() for Better Error Messages
// Bad
let file = std::fs::File::open("data.txt").unwrap();
// Good
let file = std::fs::File::open("data.txt")
.expect("data.txt should exist in current directory");
2. Document Panic Conditions
/// Divides two numbers.
///
/// # Panics
///
/// Panics if `b` is zero.
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Attempt to divide by zero");
}
a / b
}
3. Prefer Returning Result
// Bad: panics on error
fn read_number(s: &str) -> i32 {
s.parse().unwrap()
}
// Good: returns Result
fn read_number_safe(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
4. Use unwrap() Only When Certain
fn main() {
// OK: We just created it
let v = vec![1, 2, 3];
let first = v.get(0).unwrap();
// OK: We validated the regex at compile time
let re = regex::Regex::new(r"^\d+$").unwrap();
}
Guidelines
- In application code: Handle errors gracefully with Result
- In tests: unwrap() and expect() are fine
- In examples: unwrap() for simplicity is acceptable
- In libraries: Never panic on invalid input - return Result
- For bugs: panic! is appropriate for logic errors
- Document all panic conditions in public APIs