`Result<T, E>`
EssentialPrerequisites
Errors happen — a file may not exist, a network request may time out, a number may not parse. Rust encodes recoverable errors as values rather than exceptions: a function either returns Ok(T) with a success value or Err(E) with an error. The caller must handle both.
The definition
enum Result<T, E> {
Ok(T),
Err(E),
}
T is the success type; E is the error type. A function that might fail annotates its return type:
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
s.parse::<u16>()
}
Matching on a Result
Pattern matching handles both outcomes exhaustively:
match parse_port("8080") {
Ok(port) => println!("listening on {port}"),
Err(e) => eprintln!("bad port: {e}"),
}
For the “do something only on error” pattern, if let Err is concise:
if let Err(e) = risky_operation() {
eprintln!("failed: {e}");
}
The ? operator
The ? operator is the primary reason Result-based code stays readable. Inside a function that returns Result<T, E>, appending ? to any Result-producing expression either unwraps the Ok value or immediately returns the Err:
use std::fs;
use std::io;
fn read_username(path: &str) -> Result<String, io::Error> {
let contents = fs::read_to_string(path)?; // returns Err on failure
Ok(contents.trim().to_string())
}
Without ?, each step would require an explicit match. With it, the happy path reads linearly.
? also performs an automatic conversion via From: if the expression’s error type differs from the function’s declared E, Rust calls From::from to convert it. This lets a single function use ? on calls with different concrete error types, as long as each one converts into the function’s E.
Interoperability with Option
Option<T> and Result<T, E> convert into each other:
| Expression | Converts |
|---|---|
opt.ok_or(e) | Option<T> → Result<T, E> |
opt.ok_or_else(|| compute_err()) | lazy version |
result.ok() | Result<T, E> → Option<T> (discards the error) |
ok_or is common inside Result-returning functions when a step returns Option:
fn first_word(s: &str) -> Result<&str, &str> {
s.split_whitespace().next().ok_or("empty string")
}
Common methods
map transforms the Ok value without touching Err:
let doubled: Result<i32, _> = "21".parse::<i32>().map(|n| n * 2); // Ok(42)
map_err transforms the Err value, useful for normalising error types:
let result: Result<i32, String> =
"abc".parse::<i32>().map_err(|e| e.to_string());
unwrap_or returns the Ok value or a fallback if Err:
let n: i32 = "bad".parse().unwrap_or(0); // 0
and_then chains a second fallible operation that runs only on Ok:
fn validated_port(s: &str) -> Result<u16, String> {
s.parse::<u16>()
.map_err(|e| e.to_string())
.and_then(|p| {
if p >= 1024 { Ok(p) } else { Err("port must be ≥ 1024".to_string()) }
})
}
Summary
Result<T, E>encodes success asOk(T)and failure asErr(E).matchandif letdestructureResultexhaustively.?in aResult-returning function short-circuits toErrand auto-converts the error type viaFrom.opt.ok_or(e)convertsOptiontoResult;result.ok()converts back.map,map_err,and_then, andunwrap_orcomposeResultvalues without explicit matching.