Rust 33 🦀 Recoverable Errors with the Result Enum
A function that can fail has to tell the caller. Rust’s mechanism for this is Result<T, E>, an enum with two variants: Ok(T) for success and Err(E) for failure. The Result is not an exception. It is a value. The caller receives it, inspects it, and decides what to do. The compiler checks that the Err case is handled, either explicitly or by propagating it with the ? operator. The distinction between Result and panic! is the distinction between an error the program can recover from and a bug it cannot.
This chapter covers Result<T, E> in full. You will learn how to return a Result from a function, how to inspect it with match and the combinators, how to propagate it with the ? operator, how to define custom error types, and the patterns that use the Result for file I/O, parsing, and validation.
Key point: Result<T, E> is an enum with two variants: Ok(T) and Err(E). A function that can fail returns a Result, and the caller must handle both variants. The ? operator propagates the Err to the caller, converting the error type when the From trait is implemented. The unwrap and expect methods panic on Err and should be used only when the failure is not expected. The ? operator is the idiomatic way to propagate the errors without the match boilerplate.
Why Result exists
The exception problem. Languages with exceptions throw them and let the stack unwind to the nearest handler. The caller cannot tell from the function signature whether the function can fail. The failure is invisible in the type, and the caller may not know to handle it. The Result makes the failure visible. The function signature says Result<T, E>, and the caller knows the function can fail and what the error type is.
The sentinel problem. Languages without exceptions use sentinel values: -1 for a failed index, null for a missing value, an empty string for a failed parse. The sentinel is a value in the type’s domain, and the caller may not check it. The Result is a separate type. The caller cannot use the T without extracting it from the Ok, and the extraction is the check.
The propagation problem. A function that calls a fallible function must decide: handle the error or pass it up. The match on the Result is the explicit form, but it is verbose when the function only wants to propagate. The ? operator is the shorthand: it returns the Err from the current function if the result is Err, and extracts the T if the result is Ok.
The error-type problem. Different functions have different error types. A file operation returns io::Error. A parse returns ParseIntError. A function that calls both must unify the errors. The From trait and the Box<dyn Error> type are the two mechanisms. The ? operator uses the From trait to convert the error type automatically.
The composition problem. The Result has combinators: map, and_then, or_else, unwrap_or, and the rest. The combinators chain the operations that can fail, and the chain short-circuits on the first Err. The ? operator is the syntax, and the combinators are the methods.
The library problem. A library function that can fail returns Result. The caller decides whether to handle the error, to propagate it, or to panic. The library does not make the decision. The Result gives the choice to the caller, and the choice is the API contract.
a. Returning and matching a Result
A function that can fail returns a Result<T, E>, where T is the success type and E is the error type.
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.parse::<i32>()
}
The parse_number returns Result<i32, ParseIntError>. The s.parse::<i32>() returns the same type, and the function returns it directly.
The caller matches on the Result.
fn main() {
match parse_number("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}
}
The match handles both variants. The Ok binds the value, and the Err binds the error.
The parse_number("42") returns Ok(42). The parse_number("abc") returns Err(ParseIntError { ... }).
The Result can be created explicitly.
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
The divide returns Ok(a / b) when b is not zero, and Err(...) when it is. The error type is String, which is a simple choice for the example.
The caller can use the combinators.
let result = divide(10, 2);
let doubled = result.map(|n| n * 2);
println!("{:?}", doubled); // Ok(10)
The map applies the function to the Ok value and leaves the Err unchanged.
The and_then chains the operations that return Result.
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
parse_number(s).map(|n| n * 2)
}
The map is the transformation. The and_then is the chain when the function itself returns Result.
fn parse_and_divide(s: &str, b: i32) -> Result<i32, String> {
parse_number(s)
.map_err(|e| e.to_string())
.and_then(|n| divide(n, b))
}
The map_err converts the error type from ParseIntError to String. The and_then chains the divide call.
The unwrap_or provides a default for the Err case.
let n = parse_number("abc").unwrap_or(0);
println!("{}", n); // 0
The unwrap_or returns the Ok value or the default. The unwrap_or_else takes a closure for the default.
let n = parse_number("abc").unwrap_or_else(|e| {
eprintln!("Error: {}", e);
0
});
The unwrap and expect panic on the Err.
let n = parse_number("42").unwrap();
let n = parse_number("42").expect("Failed to parse");
The unwrap panics with the default message. The expect panics with the custom message.
b. The ? operator and the error propagation
The ? operator is the shorthand for the match that propagates the Err.
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
The File::open(path)? returns the File if the open succeeds, or returns the io::Error from the function if it fails. The read_to_string(&mut contents)? does the same for the read.
The ? operator is the equivalent of:
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
The ? is shorter and the error propagates automatically.
The ? operator converts the error type with the From trait. If the function returns Result<T, E1> and the ? is on a Result<T, E2>, the From<E2> for E1 must be implemented.
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
The function returns Result<i32, Box<dyn std::error::Error>>. The ? on the File::open converts the io::Error to the boxed error. The ? on the parse converts the ParseIntError to the boxed error. The Box<dyn Error> implements From for any error type that implements Error.
The Box<dyn Error> is the simple way to unify the errors. The ? works because the From is implemented.
The custom error type with the From implementation.
use std::fmt;
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(std::num::ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for AppError {}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn read_number(path: &str) -> Result<i32, AppError> {
let mut file = std::fs::File::open(path)?;
let mut contents = String::new();
std::io::Read::read_to_string(&mut file, &mut contents)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
The AppError has two variants, one for each error type. The From implementations convert the io::Error and the ParseIntError to the AppError. The ? operator uses the From to convert.
The ? operator can be used in a function that returns Option.
fn first_word_length(s: &str) -> Option<usize> {
let word = s.split_whitespace().next()?;
Some(word.len())
}
The ? on the Option returns None if the value is None. The ? works for both Result and Option.
The ? operator can be used in main if main returns Result.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("file.txt")?;
println!("{}", contents);
Ok(())
}
The main returns Result<(), Box<dyn Error>>. The ? propagates the error, and the program prints the error and exits with a non-zero status if the error occurs.
c. Custom error types and the patterns
A custom error type is an enum with a variant per error case.
#[derive(Debug)]
enum ValidationError {
Empty,
TooShort(usize),
InvalidChar(char),
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ValidationError::Empty => write!(f, "input is empty"),
ValidationError::TooShort(min) => write!(f, "input is shorter than {}", min),
ValidationError::InvalidChar(c) => write!(f, "invalid character: {}", c),
}
}
}
impl std::error::Error for ValidationError {}
The ValidationError has three variants, each with the data for the case. The Display implementation formats the error. The Error implementation makes it compatible with the ? and the Box<dyn Error>.
A function that returns the custom error.
fn validate_username(s: &str) -> Result<(), ValidationError> {
if s.is_empty() {
return Err(ValidationError::Empty);
}
if s.len() < 3 {
return Err(ValidationError::TooShort(3));
}
for c in s.chars() {
if !c.is_alphanumeric() && c != '_' {
return Err(ValidationError::InvalidChar(c));
}
}
Ok(())
}
The validate_username returns Ok(()) on success and the specific error variant on failure.
The caller matches on the error.
match validate_username("ab") {
Ok(()) => println!("Valid"),
Err(ValidationError::Empty) => println!("Empty"),
Err(ValidationError::TooShort(min)) => println!("Too short, need {}", min),
Err(ValidationError::InvalidChar(c)) => println!("Invalid char: {}", c),
}
The match handles every variant. The TooShort binds the minimum length, and the InvalidChar binds the character.
The Result in the main function.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let username = "alice";
validate_username(username)?;
println!("Username is valid");
Ok(())
}
The ? propagates the ValidationError to main, which converts it to the boxed error and prints it.
The unwrap_or_default provides the default for the Ok type.
let value: i32 = result.unwrap_or_default();
The unwrap_or_else takes a closure.
let value = result.unwrap_or_else(|e| {
eprintln!("Error: {}", e);
0
});
The ok method converts a Result to an Option, discarding the error.
let value = result.ok(); // Option<T>
The err method converts a Result to an Option of the error.
let error = result.err(); // Option<E>
The is_ok and is_err methods check the variant.
if result.is_ok() {
println!("Success");
}
The Result is the return type of the main function for the programs that use the ? operator.
The Result in the iterator methods.
let numbers: Result<Vec<i32>, _> = ["1", "2", "three"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
The collect on an iterator of Result produces a Result<Vec<i32>, E>. The Ok contains the collected values, and the Err contains the first error. The collect short-circuits on the first Err.
The partition method separates the Ok and Err.
let (ok, err): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
The partition separates the results into the successes and the failures.
Complete Example Session
// ============================================
// PART 1: RETURN RESULT
// ============================================
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
}
// ============================================
// PART 2: MATCH ON RESULT
// ============================================
match parse_number("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}
// ============================================
// PART 3: MAP
// ============================================
let doubled = parse_number("42").map(|n| n * 2);
println!("{:?}", doubled); // Ok(84)
// ============================================
// PART 4: AND_THEN
// ============================================
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
parse_number(s).map(|n| n * 2)
}
// ============================================
// PART 5: UNWRAP_OR
// ============================================
let n = parse_number("abc").unwrap_or(0);
println!("{}", n); // 0
// ============================================
// PART 6: QUESTION MARK OPERATOR
// ============================================
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// ============================================
// PART 7: BOXED ERROR
// ============================================
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string(path)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
// ============================================
// PART 8: CUSTOM ERROR
// ============================================
#[derive(Debug)]
enum ValidationError {
Empty,
TooShort(usize),
InvalidChar(char),
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ValidationError::Empty => write!(f, "empty"),
ValidationError::TooShort(min) => write!(f, "shorter than {}", min),
ValidationError::InvalidChar(c) => write!(f, "invalid char: {}", c),
}
}
}
impl std::error::Error for ValidationError {}
// ============================================
// PART 9: RESULT IN MAIN
// ============================================
fn main() -> Result<(), Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("file.txt")?;
println!("{}", contents);
Ok(())
}
// ============================================
// PART 10: COLLECT RESULTS
// ============================================
let numbers: Result<Vec<i32>, _> = ["1", "2", "three"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
// Err(ParseIntError { ... })
The ten parts covered returning a Result, matching on it, map, and_then, unwrap_or, the ? operator, the boxed error, the custom error, the Result in main, and collecting the results.
Quick Reference
Result Variants
| Variant | Meaning |
|---|---|
Ok(T) | Success |
Err(E) | Failure |
Inspection
| Method | Returns |
|---|---|
is_ok() | bool |
is_err() | bool |
ok() | Option<T> |
err() | Option<E> |
unwrap() | T or panic |
expect(msg) | T or panic |
unwrap_or(d) | T or d |
unwrap_or_else(f) | T or f(e) |
Combinators
| Method | Purpose |
|---|---|
map(f) | Transform Ok |
map_err(f) | Transform Err |
and_then(f) | Chain a Result-returning function |
or_else(f) | Recover from Err |
The ? Operator
| Context | Behavior |
|---|---|
Result<T, E> function | Returns Err early |
Option<T> function | Returns None early |
main returns Result | Prints the error |
Error Types
| Type | Use |
|---|---|
io::Error | File and network |
ParseIntError | Integer parsing |
Box<dyn Error> | Unified error |
| Custom enum | Specific errors |
Best Practices
✅ Do This:
// Return Result for fallible functions
fn parse(s: &str) -> Result<i32, ParseIntError> { } // ✅
// Use ? to propagate
let contents = fs::read_to_string(path)?; // ✅
// Use map for the Ok transformation
result.map(|n| n * 2); // ✅
// Use and_then for the chain
parse(s).and_then(|n| divide(n, 2)); // ✅
// Use unwrap_or for the default
let n = result.unwrap_or(0); // ✅
// Use a custom error enum for the specific errors
enum AppError { Io(io::Error), Parse(ParseIntError) } // ✅
// Use Box<dyn Error> for the simple unification
fn f() -> Result<(), Box<dyn Error>> { } // ✅
❌ Don’t Do This:
// Don't unwrap when the error is possible
let contents = fs::read_to_string(path).unwrap(); // ⚠️
// Don't ignore the Result
fs::write(path, data); // warning, the Result is unused // ❌
// Don't use ? in a function that does not return Result
fn f() {
let x = parse(s)?; // error // ❌
}
// Don't match when ? would do
match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e),
} // ⚠️
// Don't use panic! for the recoverable errors
if file.is_none() { panic!("Not found"); } // ❌
// Don't return a generic String for every error
fn f() -> Result<i32, String> { } // ⚠️
// Don't forget the error conversion
let n = contents.parse::<i32>()?; // if the error type differs // ⚠️
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
? in main | main returns () | Return Result |
| Error type mismatch | Different error types | Use From or Box |
unwrap panics | Error not expected | Use ? or match |
| Result ignored | No handling | Use ? or match |
Custom error no Display | Error requires it | Implement Display |
? on Option in Result | Different types | Use ok_or |
Real-World Examples
1. Parse
fn parse(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
2. Read File
let contents = fs::read_to_string("file.txt")?;
3. Divide
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 { Err("division by zero".into()) } else { Ok(a / b) }
}
4. Chain
parse(s).and_then(|n| divide(n, 2))
5. Default
let n = result.unwrap_or(0);
6. Boxed Error
fn f() -> Result<(), Box<dyn std::error::Error>> { }
7. Custom Error
enum AppError { Io(io::Error), Parse(ParseIntError) }
8. From Implementation
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
9. Main with Result
fn main() -> Result<(), Box<dyn std::error::Error>> { }
10. Collect Results
let numbers: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
Visual
Result Flow
┌─────────────────────────────────────────────────────────────┐
│ fn parse(s: &str) -> Result<i32, ParseIntError> │
│ │
│ "42" ──▶ Ok(42) │
│ "abc" ──▶ Err(ParseIntError) │
│ │
│ The caller must handle both. │
│ │
└─────────────────────────────────────────────────────────────┘
The ? Operator
┌─────────────────────────────────────────────────────────────┐
│ fn read_number(path: &str) -> Result<i32, Box<dyn Error>> {│
│ let contents = fs::read_to_string(path)?; │
│ let n = contents.trim().parse::<i32>()?; │
│ Ok(n) │
│ } │
│ │
│ The ? is the shorthand for: │
│ │
│ let contents = match fs::read_to_string(path) { │
│ Ok(c) => c, │
│ Err(e) => return Err(e.into()), │
│ }; │
│ │
│ The Err is returned early. The Ok is extracted. │
│ │
└─────────────────────────────────────────────────────────────┘
Result vs Panic
┌─────────────────────────────────────────────────────────────┐
│ RESULT │
│ │
│ Recoverable error │
│ The caller handles it │
│ fn read() -> Result<String, io::Error> │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ PANIC │
│ │
│ Unrecoverable error │
│ The program cannot continue │
│ panic!("Invariant violated") │
│ │
└─────────────────────────────────────────────────────────────┘
Error Conversion
┌─────────────────────────────────────────────────────────────┐
│ fn f() -> Result<i32, Box<dyn Error>> { │
│ let a = fs::read_to_string("a")?; // io::Error │
│ let b = a.parse::<i32>()?; // ParseIntError │
│ Ok(b) │
│ } │
│ │
│ The ? converts: │
│ io::Error → Box<dyn Error> │
│ ParseIntError → Box<dyn Error> │
│ │
│ The From implementation does the conversion. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Type | Result<T, E> |
| Variants | Ok(T), Err(E) |
| Return | fn f() -> Result<T, E> |
| Propagate | ? operator |
| Transform | map, map_err |
| Chain | and_then |
| Default | unwrap_or, unwrap_or_else |
| Convert | From trait |
| Unified | Box<dyn Error> |
| Main | fn main() -> Result<(), E> |
| Panic | unwrap, expect |
Key takeaways:
Result<T, E>is the type for fallible operations. TheOkvariant holds the success value, and theErrvariant holds the error. The caller must handle both.- The
?operator propagates the error. It returns theErrfrom the current function if the result isErr, and extracts theTif the result isOk. It is the shorthand for thematchthat propagates. - The
?operator converts the error type. TheFromtrait is used to convert the error from the function’s error type to the caller’s. TheBox<dyn Error>implementsFromfor any error type, which makes it the simple unification. - The combinators chain the operations. The
maptransforms theOk, themap_errtransforms theErr, and theand_thenchains aResult-returning function. The chain short-circuits on the firstErr. - The custom error type is an enum with a variant per case. The enum derives
Debugand implementsDisplayandError. TheFromimplementations convert the other error types to the custom one. maincan returnResult. The?operator works inmainwhen the return type isResult<(), E>. The program prints the error and exits with a non-zero status.Resultis for the recoverable errors;panic!is for the unrecoverable ones. The function returnsResultwhen the caller can decide what to do. The function panics when the error indicates a bug or a broken invariant.
Remember: The Result<T, E> is the mechanism for the errors the program can recover from. Use it for the file operations, the parsing, the network calls, and the validation. Use the ? operator to propagate the errors without the boilerplate. Use the custom error enum for the specific errors and the Box<dyn Error> for the simple unification. Use the unwrap and expect only when the error is not expected. And remember that the Result gives the choice to the caller, which is the point: the function reports the failure, and the caller decides what to do.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!