Option & Result
Rust's Option and Result types for null-safe and error-safe code — pattern matching, combinators, the ? operator, and common patterns
Rust has no null or exceptions. Instead, it uses Option<T> for values that may be absent and Result<T, E> for operations that may fail. The compiler forces you to handle both cases.
Option<T>
Option<T> replaces null. It’s either Some(value) or None.
// Creating
let some: Option<i32> = Some(42);
let none: Option<i32> = None;
// Pattern matching
match some {
Some(v) => println!("got {}", v),
None => println!("nothing"),
}
// if let (when you only care about one case)
if let Some(v) = some {
println!("got {}", v);
}
// while let
let mut iter = vec![1, 2, 3].into_iter();
while let Some(v) = iter.next() {
println!("{}", v);
}Option combinators
Avoid explicit match with these methods:
let x: Option<i32> = Some(42);
// unwrap — panics on None (use only when you're sure)
let v = x.unwrap();
let v = x.unwrap_or(0); // default value
let v = x.unwrap_or_default(); // type's default
let v = x.expect("must have a value"); // panic with message
// map — transform the inner value
let doubled = x.map(|v| v * 2); // Some(84)
// and_then — chain Options (flatmap)
fn parse(s: &str) -> Option<i32> {
s.parse().ok()
}
let result = parse("42").and_then(|n| Some(n * 2)); // Some(84)
// or_else — provide alternative on None
let result = None.or_else(|| Some(99)); // Some(99)
// filter — keep Some only if predicate passes
let result = Some(42).filter(|v| *v > 10); // Some(42)
let result = Some(5).filter(|v| *v > 10); // None
// map_or — transform with default
let result = Some(42).map_or(0, |v| v * 2); // 84
let result = None.map_or(0, |v| v * 2); // 0
// ok_or — convert Option to Result
let result = Some(42).ok_or("not found"); // Ok(42)
let result: Result<i32, &str> = None.ok_or("not found"); // Err("not found")
// Take and replace
let mut x = Some(42);
let taken = x.take(); // x is now None, taken is Some(42)
// Insert and get old value
let mut x = Some(1);
let old = x.replace(2); // x is now Some(2), old is Some(1)
Result<T, E>
Result<T, E> replaces exceptions. It’s either Ok(value) or Err(error).
use std::fs;
// Creating
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("something failed");
// Pattern matching
match fs::read_to_string("config.txt") {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("Error: {}", e),
}Result combinators
let result: Result<i32, &str> = Ok(42);
// unwrap — panics on Err
let v = result.unwrap();
let v = result.unwrap_or(0);
let v = result.expect("must succeed");
// map — transform Ok value
let doubled = result.map(|v| v * 2); // Ok(84)
// map_err — transform Err value
let result = Err("io").map_err(|e| format!("Error: {}", e));
// and_then — chain Results (flatmap)
fn read_config(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
fn parse_config(s: &str) -> Result<Config, ParseError> {
// ...
}
let config = read_config("config.toml")
.and_then(|s| parse_config(&s));
// or — provide alternative Result on Err
let result = Err("fail").or(Ok(99)); // Ok(99)
// or_else — compute alternative on Err
let result = Err("fail").or_else(|e| Ok(format!("recovered from {}", e)));
// as_ref / as_mut — convert &Result<T, E> to Result<&T, &E>
let r: Result<String, &str> = Ok("hello".to_string());
let r_ref: Result<&String, &&str> = r.as_ref();The ? operator
The ? operator is the idiomatic way to propagate errors. It unwraps Ok or early-returns Err.
// Without ? — verbose
fn read_config(path: &str) -> Result<Config, std::io::Error> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e),
};
let config = match parse_config(&content) {
Ok(c) => c,
Err(e) => return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData, e)),
};
Ok(config)
}
// With ? — concise
fn read_config(path: &str) -> Result<Config, std::io::Error> {
let content = std::fs::read_to_string(path)?;
let config = parse_config(&content)?;
Ok(config)
}
// ? also converts error types via From
fn do_stuff() -> Result<(), MyError> {
let data = std::fs::read_to_string("data.txt")?; // io::Error -> MyError
Ok(())
}? on Option
The ? operator also works on Option — returns None early:
fn last_char_of_first_line(text: &str) -> Option<char> {
let first_line = text.lines().next()?; // returns None if no lines
first_line.chars().last() // returns None if empty
}Converting between Option and Result
// Option -> Result
let opt: Option<i32> = Some(42);
let res: Result<i32, &str> = opt.ok_or("missing"); // Ok(42)
let opt: Option<i32> = None;
let res: Result<i32, &str> = opt.ok_or("missing"); // Err("missing")
// Result -> Option
let res: Result<i32, &str> = Ok(42);
let opt: Option<i32> = res.ok(); // Some(42)
let res: Result<i32, &str> = Err("fail");
let opt: Option<i32> = res.ok(); // None
// Get the error from a Result as Option
let res: Result<i32, &str> = Err("fail");
let err: Option<&str> = res.err(); // Some("fail")
Custom error types
For library code, define your own error type:
use std::fmt;
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, "IO error: {}", e),
MyError::Parse(e) => write!(f, "Parse error: {}", e),
MyError::NotFound(s) => write!(f, "Not found: {}", s),
}
}
}
impl std::error::Error for MyError {}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}
impl From<std::num::ParseIntError> for MyError {
fn from(e: std::num::ParseIntError) -> Self { MyError::Parse(e) }
}
// Now ? works automatically
fn do_thing() -> Result<i32, MyError> {
let content = std::fs::read_to_string("data.txt")?; // io::Error -> MyError
let num: i32 = content.trim().parse()?; // ParseIntError -> MyError
Ok(num)
}thiserror and anyhow
For real projects, use the thiserror (libraries) and anyhow (applications) crates:
// thiserror — for library error types
use thiserror::Error;
#[derive(Error, Debug)]
enum DataStoreError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Parse error at line {line}: {source}")]
Parse { line: usize, source: std::num::ParseIntError },
}
// anyhow — for application code, easy error chaining
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context(format!("Failed to read {}", path))?;
let config: Config = toml::from_str(&content)
.context("Failed to parse config")?;
Ok(config)
}Common patterns
Fallible construction
// Use a fallible constructor instead of panic
impl User {
fn new(name: String, age: u32) -> Result<Self, String> {
if name.is_empty() {
return Err("name cannot be empty".to_string());
}
Ok(User { name, age })
}
}
let user = User::new("Ada".to_string(), 30)?;Try operator in main
// Rust main can return Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = std::fs::read_to_string("input.txt")?;
let config = parse_config(&data)?;
run(config)?;
Ok(())
}