Introduction
Rust does not have exceptions. Instead, it uses the Result<T, E> enum for recoverable errors and panic! for unrecoverable ones. This design forces you to handle errors explicitly, leading to more robust programs.
Key Concepts
- Result<T, E>: An enum with two variants:
Ok(T)for success andErr(E)for failure. It is returned by any operation that can fail. - The
?operator: Syntactic sugar that unwrapsOkor returnsErrearly from the enclosing function, propagating the error to the caller. - unwrap / expect: Convenience methods that extract the
Okvalue or panic onErr.expectlets you attach a custom panic message.
Real World Context
Every production Rust application uses Result extensively: reading files, parsing configuration, making network requests, querying databases. The ? operator makes error propagation concise, and libraries like anyhow and thiserror build on Result to provide ergonomic error handling at scale.
Deep Dive
The Result enum is defined as:
rustenum Result<T, E> { Ok(T), Err(E), }
You handle it with match, which forces you to deal with both cases:
rustuse std::fs::File; let file = match File::open("config.toml") { Ok(f) => f, Err(e) => panic!("Cannot open config: {:?}", e), };
For quick prototyping, unwrap and expect provide shortcuts:
rustlet file = File::open("config.toml").unwrap(); let file = File::open("config.toml") .expect("config.toml must exist in project root");
The ? operator is the idiomatic way to propagate errors. It replaces verbose match arms with a single character:
rustuse std::io::{self, Read}; fn read_config() -> Result<String, io::Error> { let mut content = String::new(); File::open("config.toml")?.read_to_string(&mut content)?; Ok(content) }
Each ? either unwraps the Ok value and continues, or returns the Err from the function immediately.
Common Pitfalls
- Using
unwrapin library code —unwrappanics on error, crashing the program. In libraries, always returnResultso callers can decide how to handle failures. - Ignoring
Resultwarnings — Rust warns when aResultis unused. Never suppress this warning; either handle the error or explicitly ignore it withlet _ = ....
Best Practices
- Use
?for propagation — It keeps error-handling code concise and readable. Reservematchfor cases where you need to handle specific error variants differently. - Use
expectwith descriptive messages — When you do need to unwrap (e.g., in tests ormain), useexpect("why this should not fail")so the panic message explains the assumption that was violated.
Summary
Result<T, E>replaces exceptions with explicit, type-safe error handling.- Use
matchfor granular error handling,?for propagation, andexpectfor cases with clear invariants. - Never use
unwrapin production library code. - The
?operator works in any function that returnsResult(orOption). - Rust's error model catches unhandled errors at compile time.
Code Examples
use std::fs;
// Chaining with ?
fn read_username() -> Result<String, std::io::Error> {
fs::read_to_string("hello.txt")
}
// In main with Result return
fn main() -> Result<(), Box<dyn std::error::Error>> {
let username = read_username()?;
println!("Username: {username}");
Ok(())
}