Option and Result
Option and Result in Rust
Option
and Result
are two of Rust's most important enums, used for handling nullable values and errors respectively.
The Option Enum
Option represents an optional value: either Some
value or None
.
enum Option<T> {
Some(T),
None,
}
Basic Usage
fn main() {
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// Pattern matching
match some_number {
Some(n) => println!("Got number: {}", n),
None => println!("No number"),
}
}
Common Option Methods
unwrap()
and expect()
fn main() {
let x = Some(5);
let y: Option<i32> = None;
println!("x: {}", x.unwrap()); // 5
// y.unwrap(); // This would panic!
// expect() with custom error message
let z = Some(10);
println!("z: {}", z.expect("Should have a value"));
}
unwrap_or()
and unwrap_or_else()
fn main() {
let x: Option<i32> = None;
// Provide default value
println!("x: {}", x.unwrap_or(0)); // 0
// Compute default value
println!("x: {}", x.unwrap_or_else(|| {
println!("Computing default...");
42
}));
}
map()
and map_or()
fn main() {
let maybe_number = Some(5);
// Transform the value inside
let maybe_string = maybe_number.map(|n| n.to_string());
println!("{:?}", maybe_string); // Some("5")
// Map with default
let x: Option<i32> = None;
let result = x.map_or(0, |n| n * 2);
println!("Result: {}", result); // 0
}
and_then()
(flatMap)
fn square(x: u32) -> Option<u32> {
Some(x * x)
}
fn main() {
let number = Some(2);
let result = number.and_then(square).and_then(square);
println!("{:?}", result); // Some(16)
let none: Option<u32> = None;
let result = none.and_then(square);
println!("{:?}", result); // None
}
Practical Option Examples
Finding Values
fn find_user(id: u32) -> Option<String> {
let users = vec![
(1, "Alice"),
(2, "Bob"),
(3, "Charlie"),
];
users.into_iter()
.find(|(user_id, _)| *user_id == id)
.map(|(_, name)| name.to_string())
}
fn main() {
match find_user(2) {
Some(name) => println!("Found user: {}", name),
None => println!("User not found"),
}
}
Configuration Values
struct Config {
timeout: Option<u64>,
retries: Option<u32>,
}
impl Config {
fn get_timeout(&self) -> u64 {
self.timeout.unwrap_or(30)
}
fn get_retries(&self) -> u32 {
self.retries.unwrap_or(3)
}
}
The Result Enum
Result represents either success (Ok
) or failure (Err
).
enum Result<T, E> {
Ok(T),
Err(E),
}
Basic Usage
use std::fs::File;
use std::io::Error;
fn main() {
let file_result: Result<File, Error> = File::open("hello.txt");
match file_result {
Ok(file) => println!("File opened successfully"),
Err(error) => println!("Failed to open file: {}", error),
}
}
Common Result Methods
unwrap()
and expect()
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10.0, 2.0);
println!("10 / 2 = {}", result.unwrap()); // 5
let result = divide(10.0, 2.0);
println!("10 / 2 = {}", result.expect("Division should work"));
}
unwrap_or()
and unwrap_or_else()
fn main() {
let result: Result<i32, &str> = Err("error");
// Default value
println!("Value: {}", result.unwrap_or(0));
// Compute default from error
let value = result.unwrap_or_else(|e| {
println!("Error occurred: {}", e);
-1
});
}
map()
and map_err()
fn main() {
let result: Result<i32, &str> = Ok(5);
// Transform success value
let doubled = result.map(|n| n * 2);
println!("{:?}", doubled); // Ok(10)
// Transform error
let result: Result<i32, &str> = Err("oops");
let new_err = result.map_err(|e| format!("Error: {}", e));
println!("{:?}", new_err); // Err("Error: oops")
}
The ?
Operator
Propagate errors easily:
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
// Even shorter
fn read_username_short() -> Result<String, io::Error> {
std::fs::read_to_string("username.txt")
}
Chaining with ?
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse::<i32>()?;
Ok(n * 2)
}
fn process() -> Result<i32, std::num::ParseIntError> {
let result = parse_and_double("21")?;
Ok(result + 1)
}
Converting Between Option and Result
Option to Result
fn main() {
let option = Some(5);
let result: Result<i32, &str> = option.ok_or("No value");
println!("{:?}", result); // Ok(5)
let none: Option<i32> = None;
let result = none.ok_or_else(|| "Computed error");
println!("{:?}", result); // Err("Computed error")
}
Result to Option
fn main() {
let result: Result<i32, &str> = Ok(5);
let option = result.ok();
println!("{:?}", option); // Some(5)
let error: Result<i32, &str> = Err("error");
let option = error.ok();
println!("{:?}", option); // None
}
Advanced Patterns
Collecting Results
fn main() {
let strings = vec!["1", "2", "3"];
let numbers: Result<Vec<i32>, _> = strings
.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", numbers); // Ok([1, 2, 3])
let strings = vec!["1", "2", "bad"];
let numbers: Result<Vec<i32>, _> = strings
.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", numbers); // Err(ParseIntError)
}
Early Return Pattern
fn process_data(input: &str) -> Result<String, String> {
// Validate input
if input.is_empty() {
return Err("Input is empty".to_string());
}
// Parse number
let number: i32 = input.parse()
.map_err(|_| "Invalid number".to_string())?;
// Check range
if number < 0 || number > 100 {
return Err("Number out of range".to_string());
}
Ok(format!("Processed: {}", number))
}
Custom Error Types
#[derive(Debug)]
enum MyError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
ValidationError(String),
}
fn complex_operation() -> Result<i32, MyError> {
let content = std::fs::read_to_string("number.txt")
.map_err(MyError::IoError)?;
let number = content.trim().parse::<i32>()
.map_err(MyError::ParseError)?;
if number < 0 {
return Err(MyError::ValidationError("Negative number".to_string()));
}
Ok(number * 2)
}
Best Practices
- Use Option for nullable values, not empty strings or magic numbers
- Use Result for operations that can fail
- Prefer
?
operator over explicit match when propagating errors - Use descriptive error types instead of strings when possible
- Consider
unwrap_or
methods instead of match for simple defaults - Document when functions return None or Err
- Use
expect()
with meaningful messages during development - Avoid
unwrap()
in production code unless you're certain it won't panic