The Result Type in Rust
Most programs encounter operations that can fail: reading a file that might not exist, parsing a number from user input, connecting to a network that might be down. Languages that rely on exceptions handle these cases invisibly — the failure can propagate anywhere in the call stack, and callers have no idea a function can fail unless they read its documentation.
Rust takes a different approach. Functions that can fail return Result<T, E>,
making the possibility of failure explicit in the type signature. Every caller is
forced by the compiler to decide what to do when an error occurs. No silent
exceptions, no unhandled error codes, no surprises.
What is Result?
Result<T, E> is an enum with two variants:
// How Result is defined in the standard library
enum Result<T, E> {
Ok(T), // operation succeeded; holds the success value of type T
Err(E), // operation failed; holds the error value of type E
}
// Both variants are in the prelude — no import needed
fn main() {
let success: Result<i32, String> = Ok(42);
let failure: Result<i32, String> = Err(String::from("something went wrong"));
println!("{:?}", success); // Ok(42)
println!("{:?}", failure); // Err("something went wrong")
}Why Result is Better Than Exceptions
Explicit in the type — a function returning
Resultadvertises that it can fail; no documentation requiredCaller is forced to decide — you cannot ignore a
Resultwithout the compiler warning youComposable — Result works seamlessly with
map,and_then, and?to build pipelinesNo hidden control flow — errors travel as values, not as invisible stack-unwinding magic
No overhead for the happy path — unlike exceptions, returning
Okhas zero cost
Functions That Return Result
Standard library operations that interact with the outside world almost always
return Result.
use std::fs;
use std::num::ParseIntError;
fn main() {
// File I/O — Result<String, io::Error>
let content = fs::read_to_string("config.toml");
println!("{:?}", content); // Ok("...") or Err(os error 2)
// Parsing — Result<i32, ParseIntError>
let good: Result<i32, ParseIntError> = "42".parse();
let bad: Result<i32, ParseIntError> = "abc".parse();
println!("{:?}", good); // Ok(42)
println!("{:?}", bad); // Err(invalid digit found in string)
// Integer arithmetic that can overflow (checked versions)
let sum: Option<i32> = i32::MAX.checked_add(1); // None (overflow)
// Note: checked_add returns Option, but many numeric ops return Result in other contexts
}Matching on Result
The most explicit way to handle a Result is with match. You handle both
the success and failure branches, and the compiler ensures you cover both.
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
s.trim().parse::<u32>()
}
fn main() {
let inputs = vec!["25", " 30 ", "abc", "200"];
for input in &inputs {
match parse_age(input) {
Ok(age) if age > 120 => println!("{:?} => implausibly old: {}", input, age),
Ok(age) => println!("{:?} => valid age: {}", input, age),
Err(e) => println!("{:?} => error: {}", input, e),
}
}
// "25" => valid age: 25
// " 30 " => valid age: 30
// "abc" => error: invalid digit found in string
// "200" => implausibly old: 200
}unwrap and expect
unwrap() extracts the Ok value or panics if the result is Err.
expect("message") does the same but adds a custom panic message. Both are
appropriate in tests, prototypes, and situations where you are certain an error
cannot occur. Avoid them in production paths.
use std::fs;
fn main() {
// unwrap — panics with a generic message on Err
let n: i32 = "42".parse().unwrap();
println!("{}", n); // 42
// expect — panics with your message on Err; prefer this over unwrap
let n2: i32 = "42".parse().expect("hardcoded literal must parse as i32");
println!("{}", n2); // 42
// In tests it is common to unwrap because a failure means the test is broken
#[cfg(test)]
fn test_parse() {
let result: i32 = "100".parse().unwrap();
assert_eq!(result, 100);
}
// This would panic:
// let _bad: i32 = "abc".parse().expect("oops, 'abc' is not a number");
// thread 'main' panicked at "oops, 'abc' is not a number: ..."
}unwrap_or, unwrap_or_else, unwrap_or_default
These methods provide safe fallback values instead of panicking on Err.
fn main() {
// unwrap_or: return Ok value or a fixed fallback
let port: u16 = "8080".parse().unwrap_or(80);
println!("{}", port); // 8080
let fallback: u16 = "bad".parse().unwrap_or(80);
println!("{}", fallback); // 80
// unwrap_or_else: lazy — closure only called on Err
let value: i32 = "abc".parse().unwrap_or_else(|e| {
eprintln!("parse failed: {}", e);
-1
});
println!("{}", value); // -1
// unwrap_or_default: use the type's Default implementation
let zero: i32 = "oops".parse().unwrap_or_default();
println!("{}", zero); // 0 (i32::default() is 0)
let empty: String = "123".parse::<bool>().map(|_| String::new()).unwrap_or_default();
println!("{:?}", empty); // ""
}is_ok and is_err
When you only need a boolean answer about whether an operation succeeded, without
extracting the value, use is_ok() and is_err().
fn main() {
let good: Result<i32, &str> = Ok(10);
let bad: Result<i32, &str> = Err("failed");
println!("{}", good.is_ok()); // true
println!("{}", good.is_err()); // false
println!("{}", bad.is_ok()); // false
println!("{}", bad.is_err()); // true
// Useful for filtering
let attempts: Vec<Result<i32, _>> = vec![
"1".parse(), "two".parse(), "3".parse(), "four".parse(),
];
let success_count = attempts.iter().filter(|r| r.is_ok()).count();
println!("successful parses: {}", success_count); // successful parses: 2
}Transforming Result with map and map_err
map transforms the Ok value without affecting Err. map_err transforms
the Err value without affecting Ok. Both leave the other variant untouched.
use std::num::ParseIntError;
fn main() {
// map: transform the Ok value
let doubled: Result<i32, ParseIntError> = "21".parse::<i32>().map(|n| n * 2);
println!("{:?}", doubled); // Ok(42)
let err_passthrough: Result<i32, ParseIntError> = "oops".parse::<i32>().map(|n| n * 2);
println!("{:?}", err_passthrough); // Err(invalid digit found in string)
// map_err: convert the error type (common when unifying error types)
#[derive(Debug)]
struct AppError(String);
let converted: Result<i32, AppError> = "abc"
.parse::<i32>()
.map_err(|e| AppError(format!("parse failed: {}", e)));
println!("{:?}", converted); // Err(AppError("parse failed: invalid digit found in string"))
// Chain map and map_err together
let processed: Result<String, AppError> = " 99 "
.trim()
.parse::<i32>()
.map(|n| format!("value is {}", n))
.map_err(|e| AppError(e.to_string()));
println!("{:?}", processed); // Ok("value is 99")
}and_then — Chaining Fallible Operations
and_then passes the Ok value to a closure that returns another Result.
If the original result is Err, it short-circuits and returns the Err unchanged.
Use it to chain multiple operations that each might fail.
use std::num::ParseIntError;
fn parse_positive(s: &str) -> Result<i32, String> {
s.trim()
.parse::<i32>()
.map_err(|e| e.to_string())
.and_then(|n| {
if n > 0 {
Ok(n)
} else {
Err(format!("{} is not positive", n))
}
})
}
fn compute(s: &str) -> Result<i32, String> {
parse_positive(s)
.and_then(|n| {
if n < 1000 {
Ok(n * n)
} else {
Err(String::from("number too large to square safely"))
}
})
}
fn main() {
println!("{:?}", compute("5")); // Ok(25)
println!("{:?}", compute("abc")); // Err("invalid digit found in string")
println!("{:?}", compute("-3")); // Err("-3 is not positive")
println!("{:?}", compute("2000")); // Err("number too large to square safely")
}The ? Operator — Propagating Errors
The ? operator is the most important ergonomics feature for error handling in
Rust. Placed after a Result expression, it does the following:
- If the result is
Ok(value), it unwraps tovalueand execution continues. - If the result is
Err(e), it immediately returnsErr(e)from the enclosing function (converting the error type viaFromif needed).
The function must return Result (or Option) for ? to work.
use std::fs;
use std::io;
use std::num::ParseIntError;
// Without ?: every step requires a match
fn read_number_verbose(path: &str) -> Result<i32, String> {
let content = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return Err(e.to_string()),
};
let trimmed = content.trim();
let number = match trimmed.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e.to_string()),
};
Ok(number * 2)
}
// With ?: clean and flat — each ? handles the error propagation
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // propagate io::Error on failure
let number = content.trim().parse::<i32>()?; // propagate ParseIntError on failure
Ok(number * 2)
}
fn main() {
match read_number("number.txt") {
Ok(n) => println!("doubled: {}", n),
Err(e) => println!("error: {}", e),
}
}// ? also works with multiple error types when they implement From
use std::fmt;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MyError { fn from(e: io::Error) -> MyError { MyError::Io(e) } }
impl From<ParseIntError> for MyError { fn from(e: ParseIntError) -> MyError { MyError::Parse(e) } }
fn load_setting(path: &str) -> Result<i32, MyError> {
let raw = std::fs::read_to_string(path)?; // io::Error -> MyError::Io via From
let parsed = raw.trim().parse::<i32>()?; // ParseIntError -> MyError::Parse via From
Ok(parsed)
}Writing Functions That Return Result
The standard pattern for a fallible function is to declare its return type as
Result<SuccessType, ErrorType> and use ? inside the body to propagate errors.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
// A function that reads lines from a file and returns them as a Vec
fn read_lines(path: &str) -> Result<Vec<String>, io::Error> {
let file = File::open(path)?; // Err propagated if file not found
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line in reader.lines() {
lines.push(line?); // Err propagated if read fails
}
Ok(lines)
}
// main can also return Result — useful for scripts
fn main() -> Result<(), Box<dyn std::error::Error>> {
let lines = read_lines("input.txt")?;
for (i, line) in lines.iter().enumerate() {
println!("{}: {}", i + 1, line);
}
Ok(())
}Collecting Results from Iterators
When mapping a fallible operation over an iterator, you often want to either
collect all successes or fail fast on the first error. Rust's collect supports
this with a Result<Vec<T>, E> target type.
fn main() {
// Fail fast: if any parse fails, the whole collection is an Err
let strings = vec!["1", "2", "3", "4"];
let numbers: Result<Vec<i32>, _> = strings.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", numbers); // Ok([1, 2, 3, 4])
let mixed = vec!["1", "oops", "3"];
let numbers2: Result<Vec<i32>, _> = mixed.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", numbers2); // Err(invalid digit found in string)
// Collect successes only, ignore errors (partition approach)
let inputs = vec!["10", "bad", "30", "?", "50"];
let (successes, failures): (Vec<_>, Vec<_>) = inputs.iter()
.map(|s| s.parse::<i32>().map_err(|e| (*s, e)))
.partition(Result::is_ok);
let values: Vec<i32> = successes.into_iter().map(|r| r.unwrap()).collect();
let errors: Vec<(&str, _)> = failures.into_iter().map(|r| r.unwrap_err()).collect();
println!("parsed: {:?}", values); // parsed: [10, 30, 50]
println!("failed inputs: {:?}", errors.iter().map(|(s, _)| s).collect::<Vec<_>>());
// failed inputs: ["bad", "?"]
}Converting Between Result and Option
Result and Option are related types and Rust provides conversions in both
directions.
fn main() {
// Result -> Option: discard the error, keep only Ok as Some
let ok: Result<i32, &str> = Ok(42);
let as_option: Option<i32> = ok.ok();
println!("{:?}", as_option); // Some(42)
let err: Result<i32, &str> = Err("failed");
let as_none: Option<i32> = err.ok();
println!("{:?}", as_none); // None
// err() — convert to Option<E>, keeping only the error
let err2: Result<i32, &str> = Err("oops");
let error_option: Option<&str> = err2.err();
println!("{:?}", error_option); // Some("oops")
// Option -> Result: provide an error for the None case
let some_value: Option<i32> = Some(7);
let as_result: Result<i32, &str> = some_value.ok_or("no value");
println!("{:?}", as_result); // Ok(7)
let none_value: Option<i32> = None;
let as_err: Result<i32, &str> = none_value.ok_or("no value");
println!("{:?}", as_err); // Err("no value")
}Creating Your Own Error Types
For library and application code, define a custom error type so callers can
match on specific failure modes. At minimum, implement std::fmt::Display and
std::error::Error. Derive Debug as well.
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
MissingKey(String),
InvalidValue { key: String, reason: String },
ParseError(ParseIntError),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::MissingKey(k) =>
write!(f, "missing required config key: {}", k),
ConfigError::InvalidValue { key, reason } =>
write!(f, "invalid value for '{}': {}", key, reason),
ConfigError::ParseError(e) =>
write!(f, "parse error: {}", e),
}
}
}
impl std::error::Error for ConfigError {}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::ParseError(e)
}
}
fn get_port(raw: Option<&str>) -> Result<u16, ConfigError> {
let value = raw.ok_or_else(|| ConfigError::MissingKey(String::from("port")))?;
let port: u32 = value.parse().map_err(|e: ParseIntError| ConfigError::ParseError(e))?;
if port > 65535 {
return Err(ConfigError::InvalidValue {
key: String::from("port"),
reason: format!("{} exceeds maximum port number", port),
});
}
Ok(port as u16)
}
fn main() {
println!("{:?}", get_port(Some("8080"))); // Ok(8080)
println!("{:?}", get_port(None)); // Err(MissingKey("port"))
println!("{:?}", get_port(Some("abc"))); // Err(ParseError(...))
println!("{:?}", get_port(Some("99999"))); // Err(InvalidValue { ... })
}Chaining Results for Clean Pipelines
Combining ?, map, and_then, and map_err lets you write linear,
readable error-handling pipelines instead of deeply nested match blocks.
use std::collections::HashMap;
#[derive(Debug)]
enum PipelineError {
Missing(String),
Parse(String),
OutOfRange(i32),
}
fn get_config_value<'a>(
config: &'a HashMap<&str, &str>,
key: &str,
) -> Result<&'a str, PipelineError> {
config.get(key)
.copied()
.ok_or_else(|| PipelineError::Missing(key.to_string()))
}
fn parse_timeout(config: &HashMap<&str, &str>) -> Result<u32, PipelineError> {
get_config_value(config, "timeout")
.and_then(|s| {
s.parse::<i32>()
.map_err(|e| PipelineError::Parse(e.to_string()))
})
.and_then(|n| {
if n > 0 && n <= 300 {
Ok(n as u32)
} else {
Err(PipelineError::OutOfRange(n))
}
})
}
fn main() {
let mut config = HashMap::new();
config.insert("timeout", "30");
println!("{:?}", parse_timeout(&config)); // Ok(30)
config.insert("timeout", "999");
println!("{:?}", parse_timeout(&config)); // Err(OutOfRange(999))
let empty: HashMap<&str, &str> = HashMap::new();
println!("{:?}", parse_timeout(&empty)); // Err(Missing("timeout"))
}Result Methods at a Glance
Method | Returns | Description |
|---|---|---|
| T | Returns Ok value; panics on Err |
| T | Like unwrap but with a custom panic message |
| T | Returns Ok value or a fixed fallback |
| T | Returns Ok value or calls closure with Err |
| T | Returns Ok value or T::default() |
| bool | True if the result is Ok |
| bool | True if the result is Err |
| Result<U,E> | Transforms Ok value; Err passes through |
| Result<T,F> | Transforms Err value; Ok passes through |
| Result<U,E> | Chains a fallible operation on Ok; Err short-circuits |
| Result<T,F> | Returns self if Ok, otherwise other |
| Result<T,F> | Returns self if Ok, otherwise calls closure |
| Option<T> | Converts Ok to Some, Err to None |
| Option<E> | Converts Err to Some, Ok to None |
| T (or returns) | Unwraps Ok or returns Err from current function |