Rust: Errors and Options
Rust does not have null and it does not have exceptions. Instead it leans on
two enums from the standard library: Option<T> for values that might be
absent, and Result<T, E> for operations that might fail. The compiler forces
you to deal with both, which removes a whole category of runtime bugs.
What is Option
Option<T> represents a value that may or may not be there. It has two
variants: Some(T) when there is a value, and None when there is not.
enum Option<T> {
Some(T),
None,
}A common place you will see it is when looking up something that might not exist, like an item in a vector by index.
fn main() {
let names = vec!["Bob", "Will", "Sooty"];
let first = names.first(); // Option<&&str>
match first {
Some(name) => println!("First name: {name}"),
None => println!("The list is empty"),
}
}What is Result
Result<T, E> represents an operation that can either succeed with a value of
type T or fail with an error of type E. It has two variants: Ok(T) and
Err(E).
enum Result<T, E> {
Ok(T),
Err(E),
}You will see it everywhere fallible work happens: parsing strings, reading files, making network requests.
fn parse_age(input: &str) -> Result<u32, std::num::ParseIntError> {
input.parse::<u32>()
}
fn main() {
match parse_age("42") {
Ok(age) => println!("Age is {age}"),
Err(e) => println!("Could not parse age: {e}"),
}
}Handling values
The most explicit way to handle either type is with match, but that can get
verbose. Rust gives you a few shortcuts.
unwrap and expect
Both pull the inner value out, and both panic if the value is missing. The
difference is that expect lets you provide a message.
fn main() {
let value: Option<i32> = Some(10);
let n = value.unwrap(); // 10
let parsed: Result<i32, _> = "5".parse();
let n = parsed.expect("input should be a valid number"); // 5
}Reach for
unwrapandexpectin tests, prototypes, or when you can prove the value is present. Avoid them in code paths where a panic would be a real problem.
unwrap_or and unwrap_or_else
Provide a fallback when the value is missing instead of panicking.
fn main() {
let port: Option<u16> = None;
let port = port.unwrap_or(8080);
let parsed: Result<i32, _> = "not a number".parse();
let n = parsed.unwrap_or_else(|_| 0);
}if let
When you only care about one variant, if let is cleaner than a full match.
fn main() {
let name: Option<&str> = Some("Bob");
if let Some(n) = name {
println!("Hello, {n}");
}
}The ? operator
The ? operator is the idiomatic way to propagate errors. It unwraps an Ok
or Some value, and on Err or None it returns early from the function with
that value.
use std::fs;
use std::io;
fn read_config() -> Result<String, io::Error> {
let contents = fs::read_to_string("config.toml")?;
Ok(contents)
}That ? is shorthand for:
let contents = match fs::read_to_string("config.toml") {
Ok(c) => c,
Err(e) => return Err(e),
};It also works on Option inside a function that returns Option.
fn first_char(s: &str) -> Option<char> {
let first = s.chars().next()?;
Some(first.to_ascii_uppercase())
}The error type returned by
?must match (or be convertible into) the error type of the function. For mixing error types, look into crates likeanyhoworthiserror.
Converting between Option and Result
Sometimes you have an Option but the surrounding function returns a
Result, or the other way around. There are helpers for both directions.
fn main() {
let maybe_name: Option<&str> = None;
// Option -> Result
let result: Result<&str, &str> = maybe_name.ok_or("name was missing");
// Result -> Option
let parsed: Option<i32> = "42".parse::<i32>().ok();
}Conclusion
Option and Result are how Rust forces you to think about absence and
failure up front, instead of discovering them at runtime.
- Use `Option<T>` when a value might not exist.
- Use `Result<T, E>` when an operation might fail and you want to explain why.
- Use `match` or `if let` when you need to handle each variant explicitly.
- Use `unwrap_or` and friends when you have a sensible fallback.
- Use `?` to propagate errors up the call stack with very little ceremony.
Once you get used to these two enums, a lot of Rust's safety story clicks into place. The compiler is not getting in your way; it is just making sure the unhappy paths get handled.
Resources
Rust: Rc v Arc v Mutex Rust: anyhow