Error handling patterns
Use Option and Result for recoverable outcomes, propagate with ?, and reserve panic for unrecoverable bugs.
- Model absence with Option and recoverable failure with Result.
- Use ? to bubble an error up without writing a match every time.
- Define a custom error type with thiserror for readable, groupable failures.
- Reserve panic for bugs, and return Result for expected failure.
In plain English
Option represents a value that may or may not be present, with the variants Some for a present value and None for absence, and you must handle both before you can use the value. Result represents an operation that can succeed or fail, with Ok for success and Err for the error, and the ? operator returns early with the error if one occurred, so the happy path reads cleanly. A custom error type groups related failures, and the thiserror crate derives a clean Display and Error implementation so your errors print and propagate like standard ones. You should panic only for unrecoverable bugs and invalid internal state, and return a Result for failures that callers are expected to handle.
Step by step
- 1Wrap a value that may be missing in an Option.
- 2Return a Result from a function that can fail.
- 3Propagate errors with ? instead of a manual match.
- 4Define a custom error enum with thiserror and let #[from] convert lower-level errors.
- 5Reach for panic only when the state should be impossible.
use std::fs;
use thiserror::Error;
#[derive(Debug, Error)]
enum ReadError {
#[error("file not found: {0}")]
NotFound(String),
#[error("could not read file: {0}")]
Io(#[from] std::io::Error),
}
fn read_title(path: &str) -> Result<String, ReadError> {
let text = fs::read_to_string(path)?;
Ok(text.trim().to_string())
}
fn pick(values: Vec<i32>) -> Option<i32> {
values.first().copied()
}
fn main() {
match pick(vec![3, 5, 8]) {
Some(first) => println!("first {}", first),
None => println!("empty"),
}
match read_title("notes.txt") {
Ok(title) => println!("title {}", title),
Err(err) => println!("error {}", err),
}
}Common mistakes
- Using panic for expected failures like a missing file crashes the whole program when a Result would let the caller decide.
- Forgetting to handle both Some and None forces an explicit match, because Option hides no path.
- Using unwrap on a Result discards the error and panics if it was Err, so it is unsafe on input you do not control.
- Ignoring an Err variant lets a failure slip through silently when the ? operator or a match would have surfaced it.
Divide with a Result
Write a function divide(a: f64, b: f64) -> Result<f64, String> that returns an error message when b is zero.
fn divide(a: f64, b: f64) -> Result<f64, String> {
Ok(0.0)
}What does the ? operator do on a Result?
Make it yours.
Keep a private note for this lesson, then turn the key ideas into flashcards you can revisit.
Lock in what you just learned.
Mark this lesson complete to save your place and move one step closer to the certificate.