TL;DR:
Result<T, E>is Rust's built-in enum for operations that can succeed or fail.Ok(T)holds the success value;Err(E)holds the error. Unlike exceptions in Java/Python, errors in Rust are values; they appear in function signatures, cannot be silently ignored, and are handled withmatch,if let, or the?operator. This makes error handling explicit, composable, and zero-cost.
What Is Result<T, E> in Rust?
Result<T, E> is an enum that represents either a successful value Ok(T) or an error Err(E); it's Rust's primary mechanism for recoverable error handling.
enum Result<T, E> {
Ok(T), // operation succeeded with value T
Err(E), // operation failed with error E
}Rust has no exceptions. Instead, functions that can fail return Result. The compiler ensures you cannot use the success value without checking whether the operation succeeded; eliminating swallowed exceptions and unhandled error states.
How Do You Use Result?
The primary tools are match for exhaustive handling, if let for single-case handling, and ? for propagating errors up the call stack.
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u8, ParseIntError> {
let n: u8 = s.parse()?; // ? propagates Err automatically
Ok(n)
}
fn main() {
// Full match; handles both cases
match parse_age("25") {
Ok(age) => println!("Age: {age}"),
Err(e) => println!("Parse error: {e}"),
}
// unwrap_or; provide a default on failure
let age = parse_age("abc").unwrap_or(0);
// map; transform Ok value
let doubled = parse_age("25").map(|n| n * 2);
// is_ok / is_err; boolean check
if parse_age("30").is_ok() {
println!("Valid age");
}
}How Does the ? Operator Work?
The ? operator on a Result returns Err early from the current function if the value is an error, or unwraps Ok and continues; making error propagation concise.
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // returns Err early if file missing
let trimmed = content.trim().to_string();
Ok(trimmed)
}
// Equivalent without ?:
fn read_config_verbose(path: &str) -> Result<String, io::Error> {
match fs::read_to_string(path) {
Err(e) => return Err(e),
Ok(content) => Ok(content.trim().to_string()),
}
}? also calls .into() on the error, enabling automatic conversion between error types when the target error type implements From<SourceError>.
How Do You Define Custom Error Types?
For application code, the common approach is an enum of error variants. For library code, implement std::error::Error.
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
Database(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {msg}"),
AppError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
AppError::Database(msg) => write!(f, "Database error: {msg}"),
}
}
}
fn find_user(id: u64) -> Result<String, AppError> {
if id == 0 {
return Err(AppError::InvalidInput("id cannot be zero".to_string()));
}
Ok("Alice".to_string())
}In production Rust, crates like thiserror (derive macros for error types) and anyhow (ergonomic error boxing for applications) simplify this significantly.
What Is the Difference Between Result and Panics?
Result is for recoverable errors (bad input, missing file, network failure). Panics are for unrecoverable programming errors (index out of bounds, assertion failure).
| Situation | Use |
|---|---|
| File not found | Result<_, io::Error> |
| Invalid user input | Result<_, ValidationError> |
| Database query failed | Result<_, sqlx::Error> |
| Index out of bounds | Panic (bug in your code) |
| Assertion failed in test | Panic |
| Impossible state reached | unreachable!() macro (panics) |
Production Rust servers catch panics at the request boundary (Axum/Actix do this automatically) to prevent one bad request from crashing the whole server.
Frequently Asked Questions
Rarely. .unwrap() panics on Err; acceptable in tests or when you can prove None/Err is impossible (e.g., "42".parse::<i32>().unwrap()). In production handlers, propagate with ? or handle with match/.unwrap_or().
thiserror provides derive macros to implement std::error::Error on your custom error enums cleanly; ideal for library crates that want to expose typed errors. anyhow provides anyhow::Error, a boxed error type that erases the concrete type; ideal for application code where you just want to propagate errors up without defining custom types.
Yes. Iterator::collect::<Result<Vec<T>, E>>() collects an iterator of Result<T, E> into a Result<Vec<T>, E>; returning the first error encountered, or Ok(vec) if all succeeded.
let results: Result<Vec<i32>, _> = vec!["1", "2", "three"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
// Err(ParseIntError) because "three" fails.ok_or(err) on Option<T> gives Result<T, E>. .ok() on Result<T, E> gives Option<T> (discarding the error). Use these at boundaries between code that uses different types.
Sources
Related Glossary Terms
- Option: Rust's null-safety type, similar pattern
- Enum:
Resultis an enum - Trait:
ResultimplementsIterator,From, and more - From / Into:
Result-based APIs rely onFromconversions for ergonomic error propagation - serde_json: Parsing and serializing JSON in Rust usually returns
Result - Panic:
Resultexists so recoverable failures do not need to become panics - Regex: Compiling a regex returns
Result, which makes it a common example in Rust error handling
Keep Reading
- Rust Error Handling: Result and Option: the definitive guide to Result in Rust
- Rust Ownership and Borrowing Explained:
Result<T, E>owns both success and error values - Rust for Python Developers: try/except vs explicit Result types
