Introduction
Result<T, E> is Rust's primary error handling type, and it supports the same combinator style as Option. By chaining map, and_then, map_err, and the ? operator, you can write error handling code that is both safe and expressive without deeply nested match arms.
Key Concepts
map: Transforms theOkvalue, passing throughErrunchanged.map_err: Transforms theErrvalue, passing throughOkunchanged. Essential for converting between error types.and_then: Chains operations that returnResult, flatteningResult<Result<T, E>, E>intoResult<T, E>.?operator: Sugar for early return onErr. Equivalent tomatch result { Ok(v) => v, Err(e) => return Err(e.into()) }.
Real World Context
Every I/O operation, parse call, and network request in Rust returns a Result. Combinator chains let you build multi-step pipelines — read a file, parse its contents, validate the data — where any step can fail, and the error propagates cleanly to the caller.
Deep Dive
Transforming Ok values with map
rustlet parsed: Result<i32, _> = "42".parse::<i32>(); let doubled: Result<i32, _> = parsed.map(|n| n * 2); assert_eq!(doubled, Ok(84));
map only runs the closure on Ok values. If parsed were Err, the error passes through untouched.
Transforming errors with map_err
When combining functions with different error types, use map_err to unify them:
rustuse std::fs; fn read_config(path: &str) -> Result<Config, AppError> { let contents = fs::read_to_string(path) .map_err(|e| AppError::Io(e))?; serde_json::from_str(&contents) .map_err(|e| AppError::Parse(e)) }
map_err converts io::Error to AppError::Io and serde_json::Error to AppError::Parse, keeping the pipeline clean.
Chaining with and_then
When each step returns a Result, use and_then to chain without nesting:
rustfn parse_port(input: &str) -> Result<u16, String> { input.parse::<u16>() .map_err(|e| format!("Invalid port: {e}")) } fn validate_port(port: u16) -> Result<u16, String> { if port > 1024 { Ok(port) } else { Err(format!("Port {port} is privileged")) } } let result = parse_port("8080") .and_then(validate_port); assert_eq!(result, Ok(8080)); let invalid = parse_port("80") .and_then(validate_port); assert_eq!(invalid, Err("Port 80 is privileged".to_string()));
If parse_port fails, validate_port is never called.
The ? operator
The ? operator is the most common way to propagate errors in Rust:
rustfn process_file(path: &str) -> Result<Vec<i32>, Box<dyn std::error::Error>> { let content = fs::read_to_string(path)?; // Returns early on Err let numbers: Vec<i32> = content .lines() .map(|line| line.trim().parse::<i32>()) .collect::<Result<Vec<_>, _>>()?; // Collects Results, returns on first Err Ok(numbers) }
Each ? either unwraps the Ok value or returns the error to the caller. The Into trait is called on the error, allowing automatic conversion.
Collecting Results from iterators
You can collect an iterator of Result values into a single Result containing a collection:
rustlet inputs = vec!["1", "2", "three", "4"]; let parsed: Result<Vec<i32>, _> = inputs.iter() .map(|s| s.parse::<i32>()) .collect(); // Err — "three" fails to parse, so the whole collection fails assert!(parsed.is_err());
This pattern is extremely useful for batch validation.
Common Pitfalls
- Using
unwrap()for error handling —unwrap()panics onErr. Use?,unwrap_or_else, or explicit matching instead. - Forgetting
map_errwhen error types differ — If function A returnsio::Errorand function B returnsserde::Error, you needmap_errto convert to a common error type. - Using
and_thenwhenmapsuffices — If your closure returns a plain value (not aResult), usemap.and_thenis for closures returningResult.
Best Practices
- Prefer
?for straightforward propagation — For most functions,?is cleaner than explicit combinator chains. - Use
map_errat API boundaries — Convert library errors to your domain error type at the point of use. - Combine
inspect/inspect_errfor logging —result.inspect_err(|e| log::warn!("Failed: {e}"))logs without consuming the error.
Summary
maptransforms Ok values;map_errtransforms Err values.and_thenchains fallible operations without nesting.- The
?operator is syntactic sugar for early return on Err. - Collecting
Resultiterators yieldsResult<Collection, E>— fails on the first error. - Use
inspect_errfor non-destructive error logging.
Code Examples
use std::fs;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(ParseIntError),
Validation(String),
}
// Full pipeline: read file -> parse lines -> validate -> collect
fn load_scores(path: &str) -> Result<Vec<u32>, AppError> {
fs::read_to_string(path)
.map_err(AppError::Io)
.and_then(|content| {
content.lines()
.map(|line| {
line.trim()
.parse::<u32>()
.map_err(AppError::Parse)
.and_then(|score| {
if score <= 100 {
Ok(score)
} else {
Err(AppError::Validation(
format!("Score {score} exceeds 100")
))
}
})
})
.collect()
})
}
// Using ? for the same logic — often cleaner
fn load_scores_with_question_mark(path: &str) -> Result<Vec<u32>, AppError> {
let content = fs::read_to_string(path).map_err(AppError::Io)?;
content.lines()
.map(|line| {
let score: u32 = line.trim().parse().map_err(AppError::Parse)?;
if score <= 100 {
Ok(score)
} else {
Err(AppError::Validation(format!("Score {score} exceeds 100")))
}
})
.collect()
}