#16 Error handling
Result, ?, custom errors
Panic vs Result
Rust has two mechanisms for handling errors: panic! for unrecoverable errors and Result for recoverable ones. Most of the time, you should use Result.
fn main() {
// panic! immediately stops the program
// panic!("Something went terribly wrong!");
// These also panic:
let v = vec![1, 2, 3];
// let x = v[99]; // index out of bounds -> panic
// Use Result instead for recoverable situations
let result = "42".parse::<i32>();
match result {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Failed: {}", e),
}
}Rule of thumb: use panic! in examples, tests, or truly unrecoverable situations. Use Result in library code and production applications.
Custom error types
For real applications, you define your own error types. An enum is perfect for representing different kinds of errors:
use std::fmt;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
ConfigError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "I/O error: {}", e),
AppError::ParseError(e) => write!(f, "Parse error: {}", e),
AppError::ConfigError(msg) => {
write!(f, "Configuration error: {}", msg)
}
}
}
}Implementing Display lets you print user-friendly error messages. Implementing Debug (via #[derive]) gives you detailed output for debugging.
The ? operator in depth
The ? operator does more than just propagate errors. It also calls From::from() to convert the error type automatically:
use std::fs;
fn read_port_from_config() -> Result<u16, AppError> {
// ? converts io::Error to AppError automatically
let content = fs::read_to_string("config.txt")?;
// ? converts ParseIntError to AppError automatically
let port: u16 = content.trim().parse()?;
Ok(port)
}For this to work, you need From implementations that convert each source error into your custom error type.
Error conversion with From
The From trait tells Rust how to convert one error type into another. This is what makes ? so ergonomic:
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::IoError(error)
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(error: std::num::ParseIntError) -> Self {
AppError::ParseError(error)
}
}
// Now ? works seamlessly:
fn load_config() -> Result<String, AppError> {
let content = std::fs::read_to_string("config.txt")?; // io::Error -> AppError
let _port: u16 = content.trim().parse()?; // ParseIntError -> AppError
Ok(content)
}You can chain multiple ? operators in a single function, and each one converts its error type through From:
fn process_data() -> Result<String, AppError> {
let raw = std::fs::read_to_string("data.txt")?;
let count: i32 = raw.lines().count().to_string().parse()?;
Ok(format!("Processed {} lines", count))
}The anyhow crate
For applications (not libraries), the anyhow crate simplifies error handling dramatically. It provides a single anyhow::Error type that can hold any error:
[dependencies]
anyhow = "1"use anyhow::{Context, Result};
use std::fs;
// Result is anyhow::Result<T>, which is Result<T, anyhow::Error>
fn read_config() -> Result<String> {
let content = fs::read_to_string("config.toml")
.context("Failed to read config file")?;
Ok(content)
}
fn parse_port(config: &str) -> Result<u16> {
let port: u16 = config
.trim()
.parse()
.context("Failed to parse port number")?;
Ok(port)
}
fn main() -> Result<()> {
let config = read_config()?;
let port = parse_port(&config)?;
println!("Server running on port {}", port);
Ok(())
}The .context() method adds human-readable messages to errors, creating a chain of context that makes debugging easier. For libraries, prefer custom error types so users can match on specific errors.
Summary
panic!("msg") // Unrecoverable error (avoid in libs)
Result<T, E> // Recoverable error
// Custom errors
#[derive(Debug)]
enum MyError { Variant(source) }
impl Display for MyError { ... }
impl From<OtherError> for MyError { ... }
// The ? operator
let val = might_fail()?; // Propagate error, auto-convert via From
// anyhow (for applications)
use anyhow::{Context, Result};
fn foo() -> Result<T> { ... }
.context("message")? // Add context to errorsYour turn
Try running the error handling demo with cargo run and verify with cargo check: