Error Handling in Rust: Result, the ? Operator, thiserror and anyhow
Rust has no exceptions. Functions that can fail return Result<T, E>, and the compiler makes sure you deal with the error case.
Result and match
use std::fs;
match fs::read_to_string("config.toml") {
Ok(text) => println!("{} bytes", text.len()),
Err(e) => eprintln!("could not read config: {e}"),
}
The ? operator
? returns the error to the caller if there is one, otherwise unwraps the value. It keeps the happy path readable:
use std::{fs, io};
fn read_port() -> Result<u16, Box<dyn std::error::Error>> {
let text = fs::read_to_string("port.txt")?; // io::Error
let port = text.trim().parse::<u16>()?; // ParseIntError
Ok(port)
}
Libraries: precise errors with thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PaymentError {
#[error("card declined: {0}")]
Declined(String),
#[error("amount must be positive, got {0}")]
InvalidAmount(i64),
#[error("gateway unavailable")]
Gateway(#[from] reqwest::Error), // ? converts reqwest errors automatically
}
pub async fn charge(amount: i64) -> Result<String, PaymentError> {
if amount <= 0 {
return Err(PaymentError::InvalidAmount(amount));
}
let id = gateway::charge(amount).await?;
Ok(id)
}
Callers can match on each variant and react differently.
Applications: easy context with anyhow
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = std::fs::read_to_string("config.toml")
.context("failed to read config.toml")?;
let port: u16 = config.trim().parse().context("port must be a number")?;
println!("listening on {port}");
Ok(())
}
Guidelines
- Libraries:
thiserrorenums so users can handle specific cases. - Binaries and top-level code:
anyhowwith.context()for helpful messages. - Use
unwrap()andexpect()only for genuine bugs (or in tests), never for expected failures like bad user input. Option<T>works with?too, inside functions that returnOption.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.