Result Type
The Result<T, E> type is Rust's way of handling
recoverable errors. Unlike panics, which stop execution, Result allows you to
return either a success value (Ok(T)) or an error value
(Err(E)), letting the caller decide how to handle the error. This is
the preferred way to handle errors in Rust.
What is Result?
Result<T, E> is an enum with two variants:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T)- Contains the success value of typeTErr(E)- Contains the error value of typeE
Result is so commonly used that it's
included in the prelude - you don't need to import it!
Creating Results
You can create Result values directly:
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("something went wrong");
Functions That Return Result
Many standard library functions return Result. For example, file
operations:
use std::fs::File;
let file_result = File::open("hello.txt");
// file_result is Result<File, std::io::Error>
You can also write functions that return Result:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
Click Run to execute your code
Handling Results with match
The most explicit way to handle a Result is with match:
let result = divide(10, 2);
match result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
match ensures you handle both
Ok and Err cases. The compiler will error if you miss
one!
Handling Different Error Types
When working with I/O operations, you can match on different error kinds:
use std::fs::File;
use std::io::ErrorKind;
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("File not found");
// Handle file not found
return;
}
ErrorKind::PermissionDenied => {
println!("Permission denied");
return;
}
other_error => {
println!("Other error: {:?}", other_error);
return;
}
},
};
Result Helper Methods
Result provides many useful methods for handling errors:
unwrap_or() - Provide Default Value
let result: Result<i32, &str> = Err("error");
let value = result.unwrap_or(0); // Returns 0 if Err
unwrap_or_else() - Compute Default
let value = result.unwrap_or_else(|error| {
println!("Error: {}", error);
0 // Compute default value
});
map() - Transform Ok Value
let result: Result<i32, &str> = Ok(5);
let doubled = result.map(|x| x * 2); // Ok(10)
map_err() - Transform Err Value
let result: Result<i32, &str> = Err("error");
let mapped = result.map_err(|e| format!("Error: {}", e));
and_then() - Chain Results
let result: Result<i32, &str> = Ok(5);
let chained = result.and_then(|x| {
if x > 0 {
Ok(x * 2)
} else {
Err("must be positive")
}
});
or_else() - Handle Error Case
let result: Result<i32, &str> = Err("error");
let recovered = result.or_else(|e| {
println!("Recovering from: {}", e);
Ok(0) // Return alternative Result
});
is_ok() and is_err() - Check Without Unwrapping
if result.is_ok() {
println!("Success!");
}
if result.is_err() {
println!("Error occurred");
}
Click Run to execute your code
unwrap() and
expect() exist on Result, they cause panics. Use them
only in examples, tests, or when you're absolutely certain the value is
Ok.
Using if let
When you only care about one variant, if let is more concise:
if let Ok(value) = result {
println!("Success: {}", value);
}
if let Err(error) = result {
println!("Error: {}", error);
}
Chaining Results
You can chain multiple operations that return Result using
and_then():
fn parse_number(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}
fn double(n: i32) -> Result<i32, String> {
Ok(n * 2)
}
let result = parse_number("5")
.and_then(double)
.and_then(|n| Ok(n + 1));
match result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
and_then() allows you to chain
operations that return Result, making error handling more elegant
and readable.
When to Use Result
Use Result<T, E> when:
- Errors are recoverable: The caller can handle the error
- I/O operations: File operations, network requests, etc.
- Parsing/validation: User input, data parsing, etc.
- Production code: When you need robust error handling
- Library functions: When callers need to handle errors
Use panic! when:
- Unrecoverable errors: When there's no way to recover
- Programming errors: Bugs in your code
- Examples/tests: When panics are acceptable
Best Practices
- Always handle both cases: Use match or if let to handle Ok and Err
- Use helper methods: unwrap_or, map, and_then make code cleaner
- Chain operations: Use and_then() to chain Result-returning functions
- Provide context: Use map_err() to add context to errors
- Avoid unwrap() in production: Always handle errors properly
- Use ? operator: For error propagation (covered in next lesson)
Common Mistakes
1. Forgetting to handle the Err case
// Wrong - Only handles Ok
let result: Result<i32, &str> = Err("error");
let value = result.unwrap(); // Panics!
// Correct - Handle both cases
match result {
Ok(value) => println!("{}", value),
Err(error) => println!("Error: {}", error),
}
2. Using unwrap() everywhere
// Wrong - Can panic
let file = File::open("file.txt").unwrap();
// Correct - Handle the error
let file = match File::open("file.txt") {
Ok(file) => file,
Err(error) => {
println!("Failed to open file: {}", error);
return;
}
};
3. Not providing error context
// Wrong - Generic error
fn parse(s: &str) -> Result<i32, &str> {
s.parse().map_err(|_| "error")
}
// Correct - Descriptive error
fn parse(s: &str) -> Result<i32, String> {
s.parse::<i32>()
.map_err(|e| format!("Failed to parse '{}': {}", s, e))
}
4. Not chaining Results properly
// Wrong - Nested matches
let result1 = parse_number("5");
match result1 {
Ok(n) => {
let result2 = double(n);
match result2 {
Ok(v) => println!("{}", v),
Err(e) => println!("{}", e),
}
}
Err(e) => println!("{}", e),
}
// Correct - Use and_then
parse_number("5")
.and_then(double)
.map(|v| println!("{}", v))
.map_err(|e| println!("{}", e));
Exercise: Result Handling Practice
Task: Implement functions that return Result and handle errors properly.
Requirements:
- Write a function to parse numbers from strings
- Write a safe division function
- Chain operations using and_then()
- Handle all error cases properly
Click Run to execute your code
Show Solution
fn parse_number(s: &str) -> Result<i32, String> {
s.parse::<i32>()
.map_err(|e| format!("Failed to parse '{}': {}", s, e))
}
fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
fn parse_and_divide(s: &str) -> Result<i32, String> {
parse_number(s)
.and_then(|n| {
if n == 0 {
Err(String::from("Cannot divide by zero"))
} else {
safe_divide(100.0, n as f64)
.map(|result| result as i32)
}
})
}
Summary
- Result<T, E> represents success (Ok) or failure (Err)
- Use
matchto handle both cases exhaustively - Use helper methods:
unwrap_or(),map(),and_then() - Chain operations with
and_then()for cleaner code - Use
map_err()to add context to errors - Result is for recoverable errors, panic is for unrecoverable
- Always handle both Ok and Err cases
- Avoid
unwrap()in production code - Many standard library functions return Result
What's Next?
Now that you understand Result, you'll learn about error propagation using the ? operator. This powerful operator allows you to propagate errors up the call stack automatically, making error handling much more concise and readable.
Enjoying these tutorials?