Handle errors gracefully with Result, Option, the ? operator, and custom error types.
Rust requires you to acknowledge and handle the possibility of failure. This explicit approach prevents bugs where errors are silently ignored. Rust groups errors into two categories: recoverable and unrecoverable.
When something goes catastrophically wrong and there's no way to recover, use panic!. It terminates the program:
fn main() {
panic!("crash and burn"); // Program terminates here
}
Panics often result from accessing out-of-bounds array indices or unwrapping None values:
fn main() {
let v = vec![1, 2, 3];
// This will panic - index out of bounds
// println!("{}", v[99]);
// This will panic - None value
let option: Option<i32> = None;
// let value = option.unwrap(); // Panic!
}
Use panic! for truly unrecoverable situations. For most error cases, use Result<T, E> instead.
The Result enum has two variants and is Rust's standard way to handle recoverable errors:
enum Result<T, E> {
Ok(T), // Operation succeeded with value T
Err(E), // Operation failed with error E
}
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("File opened successfully"),
Err(error) => println!("Error opening file: {:?}", error),
}
}
unwrap() returns the Ok value or panics. expect() does the same but with a custom error message:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")
.expect("Failed to open hello.txt"); // Panic with message if error
// Or use unwrap (less informative)
// let f = File::open("hello.txt").unwrap();
}
Avoid unwrap() in production code. It will crash your program without a useful error message. Prefer expect() with a descriptive message, or handle the error explicitly.
The ? operator simplifies error propagation. If a function returns an error, ? returns that error from the current function:
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?; // Propagate error
let mut s = String::new();
f.read_to_string(&mut s)?; // Propagate error
Ok(s)
fn main() {
match read_username_from_file() {
Ok(username) => println!("Username: {}", username),
Err(error) => println!("Error: {:?}", error),
}
}
Use methods like and_then and map to work with Results functionally:
fn parse_and_square(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
.map(|n| n * n) // Transform Ok value
fn main() {
match parse_and_square("5") {
Ok(result) => println!("Result: {}", result), // Prints: 25
Err(e) => println!("Error: {}", e),
}
}
The ? operator is syntactic sugar that replaces verbose match blocks. It automatically converts the error type if the From trait is implemented, making error propagation clean and composable.
For complex applications, define custom error types:
use std::fmt;
struct CustomError {
message: String,
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Custom Error: {}", self.message)
}
impl fmt::Debug for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{{ file: {}, line: {} }}", file!(), line!())
}
fn main() {
let error = CustomError {
message: String::from("Something went wrong"),
};
println!("{}", error); // Uses Display
println!("{:?}", error); // Uses Debug
}
For production code, use popular error handling crates. First, add them to your Cargo.toml:
[dependencies]
thiserror = "1.0"
anyhow = "1.0"
thiserror makes it easy to define custom error types:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("Invalid data: {0}")]
Invalid(String),
fn process_data(s: &str) -> Result<i32, DataError> {
s.parse()
.map_err(|_| DataError::Invalid(String::from("Not a number")))
fn main() {
match process_data("not_a_number") {
Ok(num) => println!("Parsed: {}", num),
Err(e) => eprintln!("Error: {}", e),
}
}
anyhow provides flexible error handling for quick development:
use anyhow::{Result, Context};
use std::fs;
fn read_config(path: &str) -> Result<String> {
let contents = fs::read_to_string(path)
.context("Failed to read config file")?;
Ok(contents)
fn main() {
match read_config("config.txt") {
Ok(config) => println!("Config: {}", config),
Err(e) => eprintln!("Error: {}", e),
}
}
Use thiserror for libraries (structured, typed errors) and anyhow for applications (flexible, quick error handling). They complement each other well.
Here's a practical example combining multiple concepts:
use std::fs::File;
use std::io::{self, Read};
fn read_and_parse_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
// Open file, propagating IO errors
let mut file = File::open(path)?;
// Read contents, propagating IO errors
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// Parse to integer, propagating parse errors
let number = contents.trim().parse::<i32>()?;
Ok(number)
fn main() {
match read_and_parse_file("number.txt") {
Ok(num) => println!("Number: {}", num),
Err(e) => {
eprintln!("An error occurred: {}", e);
std::process::exit(1);
}
}
}
When designing error handling for your application:
Result.? operator to let callers decide how to handle errors.unwrap() when you're absolutely certain a value exists.log crate or similar to record errors for debugging.Rust's error handling philosophy is "make invalid states unrepresentable." By encoding failure in the type system with Result and Option, the compiler catches missing error handling at compile time, not at runtime.