Error Handling in Rust
Rust has a powerful error handling system that distinguishes between unrecoverable and recoverable errors. This makes your code more robust and explicit about failure cases.
For unrecoverable errors, use panic!(). For recoverable errors, use the Result type. This distinction helps you write clearer, more maintainable code.
fn main() {
// Unrecoverable error
// panic!("Something went terribly wrong!");
// Recoverable error
let result = divide(10.0, 0.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
Try it Yourself ->
Custom Error Types
Creating custom error types gives you more control over error handling. You can define specific error variants for different failure cases.
#[derive(Debug)]
enum AppError {
NotFound(String),
PermissionDenied,
NetworkError(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(name) => write!(f, "{} not found", name),
AppError::PermissionDenied => write!(f, "Permission denied"),
AppError::NetworkError(msg) => write!(f, "Network error: {}", msg),
}
}
}
fn main() {
let error = AppError::NotFound("user".to_string());
println!("Error: {}", error);
}
Try it Yourself ->
The ? Operator
The ? operator is syntactic sugar for error propagation. It makes code cleaner by avoiding explicit match statements for every error.
use std::fs;
fn read_config() -> Result<String, std::io::Error> {
let content = fs::read_to_string("config.txt")?;
Ok(content)
}
fn main() {
match read_config() {
Ok(config) => println!("Config: {}", config),
Err(e) => println!("Failed to read config: {}", e),
}
}
Try it Yourself ->
The From Trait for Error Conversion
The From trait allows automatic error conversion, making the ? operator work seamlessly across different error types.
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::Custom(msg) => write!(f, "Error: {}", msg),
}
}
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
Try it Yourself ->
When to Use panic! vs Result
Use panic! for truly unrecoverable situations: bugs, invalid states, or when continuing would cause data corruption. Use Result for expected failures like file not found or network errors.
Most of the time, Result is the right choice. It forces you to handle errors explicitly, making your code more reliable and easier to debug.
Error Crate Ecosystem
The Rust ecosystem has excellent error handling libraries. The thiserror crate makes custom errors easier, and anyhow provides ergonomic error handling for applications.
For quick prototyping, anyhow::Result is fantastic. For libraries, thiserror gives you clean, customizable error types.