Error handling
| Errors as enums | |
| Display them | |
| Chain with source | |
| Bubble with ? |
Errors as enums
A custom error type is often a plain enum where each variant names one failure. Variants carry no payload here, so matching on them stays trivial. Functions return Result with the success type on the left and the error enum on the right.
#[derive(Debug)]
enum AppError {
NotFound,
Denied,
}
fn find_user(id: u32) -> Result<String, AppError> {
if id == 0 {
return Err(AppError::NotFound);
}
Ok(String::from("ada"))
}
Zero means missing here, and anything else maps to a fixed name. Callers match on the two variants and decide what to show. New failures become new variants, so the compiler points at every match that needs an update.
Display them
Result errors print nicely only when the type implements Display. The fmt method receives the value and a buffer, and each arm writes one line of text. The write macro returns a result itself, so each arm result becomes the function result.
use std::fmt;
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound => write!(f, "not found"),
AppError::Denied => write!(f, "access denied"),
}
}
}
With Display in place, println can format the error with empty braces. Users see short sentences instead of debug structs. The next section chains this error with other error types.
Chain with source
The std error trait marks a type as a full error citizen. Its default source method returns nothing, which fits errors with no deeper cause. Boxed trait objects then carry any such error up the call stack while keeping its message.
impl std::error::Error for AppError {}
fn open_config() -> Result<String, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string("app.conf")?;
Ok(text)
}
One boxed error type now covers file failures and custom failures together. The question mark converts each concrete error into the box automatically. Callers see a single error type no matter what went wrong inside.
Bubble with ?
The question mark returns early when a result holds an error. Happy path code stays flat instead of nesting inside match blocks. Main itself may return a result, which turns a leftover error into a clean process exit.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let text = open_config()?;
println!("{}", text);
Ok(())
}
Each question mark replaces a whole match that would return Err by hand. Successful values unwrap in place and keep their names. Reserve expect and unwrap for tests and examples, and bubble with question marks in real code.
Next: Traits
Article author: Arthur Isaev