Result & ?
Result<T, E> is Rust’s answer to error handling: a value is either Ok(T) or Err(E), and the compiler refuses to let you ignore the error path. The ? operator and the ? trait stack make propagation as ergonomic as exceptions, with none of the surprises.
Result + ?, error types, anyhow/thiserror
EXAMPLE
use std::fs;
use std::io;
use std::num::ParseIntError;
use std::path::Path;
// 1) The basic shape
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
s.parse::<u32>()
}
fn main1() {
match parse_age("42") {
Ok(n) => println!("age = {}", n),
Err(e) => println!("error: {}", e),
}
}
// 2) The ? operator — early return on error
fn read_age(path: &Path) -> Result<u32, Box<dyn std::error::Error>> {
let s = fs::read_to_string(path)?; // io::Error -> Box<dyn Error>
let n = s.trim().parse::<u32>()?; // ParseIntError -> Box<dyn Error>
Ok(n)
}
// 3) Custom error type — manual
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
NotFound(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "I/O error: {}", e),
AppError::Parse(e) => write!(f, "parse error: {}", e),
AppError::NotFound(s) => write!(f, "not found: {}", s),
}
}
}
impl std::error::Error for AppError {}
impl From<io::Error> for AppError { fn from(e: io::Error) -> Self { Self::Io(e) } }
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { Self::Parse(e) } }
fn load_user(path: &Path) -> Result<u32, AppError> {
let s = fs::read_to_string(path)?; // io::Error => AppError::Io via From
let n = s.trim().parse()?; // ParseIntError => AppError::Parse via From
Ok(n)
}
// 4) thiserror — derive everything (recommended for libraries)
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError2 {
#[error("I/O error")]
Io(#[from] io::Error),
#[error("parse error")]
Parse(#[from] ParseIntError),
#[error("not found: {0}")]
NotFound(String),
#[error("invalid config at {path}: {reason}")]
InvalidConfig { path: String, reason: String },
}
fn config_age(path: &Path) -> Result<u32, AppError2> {
let s = fs::read_to_string(path)?;
if s.trim().is_empty() {
return Err(AppError2::InvalidConfig {
path: path.display().to_string(),
reason: "empty file".into(),
});
}
Ok(s.trim().parse()?)
}
// 5) anyhow — quick error type for binaries
use anyhow::{Context, Result, anyhow, bail};
fn load(path: &Path) -> Result<u32> {
let s = fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
let n: u32 = s.trim().parse()
.with_context(|| format!("parsing {} as u32", s.trim()))?;
if n == 0 { bail!("age cannot be zero"); }
Ok(n)
}
fn main_anyhow() -> Result<()> {
let n = load(Path::new("./age.txt"))?;
println!("age = {}", n);
Ok(())
}
// anyhow::Result<T> is the right default for application code; thiserror for libraries.
// 6) Helpful Result methods
let r: Result<i32, &str> = Ok(2);
r.is_ok(); // true
r.is_err(); // false
r.unwrap(); // 2 — panics on Err (DON'T ship)
r.unwrap_or(0); // 2 (or 0 if Err)
r.unwrap_or_else(|_| 0);
r.unwrap_or_default(); // 2 (or default::default() if Err)
r.map(|n| n + 1); // Ok(3)
r.map_err(|_| "renamed");
r.and_then(|n| if n > 0 { Ok(n) } else { Err("non-positive") });
r.ok(); // Option<i32> → Some(2)
r.err(); // Option<&str> → None
// 7) Combining Results
fn fetch() -> Result<String, AppError2> {
Ok("42".into())
}
fn validate(s: String) -> Result<u32, AppError2> {
s.trim().parse().map_err(AppError2::Parse)
}
let age = fetch().and_then(validate)?;
// 8) Collecting an iterator of Results
let inputs = vec!["1", "2", "bad", "4"];
let results: Result<Vec<u32>, _> = inputs.iter().map(|s| s.parse::<u32>()).collect();
// First Err short-circuits; the result is the first error or Ok(Vec).
// Want all errors instead?
let (ok, err): (Vec<_>, Vec<_>) = inputs
.iter()
.map(|s| s.parse::<u32>())
.partition(Result::is_ok);
let ok: Vec<u32> = ok.into_iter().map(|r| r.unwrap()).collect();
let err: Vec<ParseIntError> = err.into_iter().map(|r| r.err().unwrap()).collect();
// 9) From Option to Result (and back)
let some: Option<u32> = Some(5);
let r: Result<u32, &str> = some.ok_or("missing"); // Ok(5)
let r: Result<u32, &str> = some.ok_or_else(|| "missing"); // lazy
let o: Option<u32> = r.ok();
// 10) Recoverable vs unrecoverable
// Result — recoverable; the caller chooses
// panic! — unrecoverable; program (or thread) aborts
// expect — like unwrap but with a message; use in BINARY's main() or tests, not libraries
// debug_assert! — debug-only invariant checks
// 11) Standard error patterns
fn run() -> Result<(), Box<dyn std::error::Error>> {
let conf = std::env::var("DATABASE_URL")?; // missing env -> early return
let port: u16 = std::env::var("PORT")?.parse()?;
println!("connecting to {} on {}", conf, port);
Ok(())
}
// 12) ? in main
fn main() -> Result<(), Box<dyn std::error::Error>> {
let age = load(Path::new("./age.txt"))?;
println!("{}", age);
Ok(())
}
// 13) Async
use tokio::fs;
async fn read_async(path: &Path) -> Result<u32, AppError2> {
let s = fs::read_to_string(path).await?;
Ok(s.trim().parse()?)
}
// 14) Common bugs
// • .unwrap() in production code — panics at runtime; use ? or unwrap_or instead
// • Returning Result<T, String> in libraries — convenient short-term, hides error structure
// • Custom error without From impls → can't use ? to convert; provide From for each source error
// • Ignoring an error with let _ = doThing() — Clippy warns; consider why the error doesn't matter
// • Mixing anyhow and thiserror in a library API — keep anyhow for binaries; libraries expose a custom error
// • Boxing errors with no source chain — derive thiserror or implement source() to keep context
// • Forgetting #[from] on a thiserror variant — ? requires From; without it you must .map_err
Why it matters
Use ? everywhere it makes sense, derive your library errors with thiserror, and reach for anyhow::Result<T> in binaries where you just want context-rich, dynamic errors. Reserve unwrap and expect for tests, examples, and main — production code paths should always give the caller a chance to recover.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
use std::num::ParseIntError;
fn parse(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?; // ? early-returns Err
Ok(n * 2)
}
Try it Yourself »
Exercise
Early-return on error.
let n: i32 = s.parse()
;
One character.
Discussion
Loading…