The Option and Result Types
How Rust represents "maybe nothing" and "maybe failure" without null or exceptions, using two enums from the standard library.
3 min read
Rust has no null. Instead of a value that might secretly be nothing, absence and failure are represented explicitly in the type system, using two enums you were introduced to conceptually in the enums lesson: Option<T> and Result<T, E>.
Option: maybe a value, maybe nothing
enum Option<T> {
Some(T),
None,
}Any function that might not have a value to return uses Option<T> instead of returning null or a sentinel value:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Ada"))
} else {
None
}
}The critical part: you can't accidentally use the inner value without acknowledging the None case first. This doesn't compile:
let user = find_user(2);
println!("{}", user.len()); // error: no method `len` on `Option<String>`You have to unwrap it deliberately, most commonly with match or if let:
fn main() {
match find_user(2) {
Some(name) => println!("Found: {name}"),
None => println!("No user found"),
}
}This is the entire point: the classic null-pointer bug — calling a method on something that turns out to be null — simply cannot compile in Rust. The compiler makes you handle the empty case at the point where the value could be empty, not three call sites away when it crashes.
Result: maybe a value, maybe an error
Result<T, E> is the equivalent tool for operations that can fail with a specific error, rather than just come up empty:
enum Result<T, E> {
Ok(T),
Err(E),
}fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("cannot divide by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Result: {result}"),
Err(e) => println!("Error: {e}"),
}
}Where Option tells you whether something is there, Result also tells you why it isn't — the Err variant carries an error value describing what went wrong.
Shortcuts, and why they're dangerous in production
.unwrap() returns the inner value of Some/Ok, or panics (crashing the program) on None/Err:
let user = find_user(1).unwrap(); // fine here — id 1 is known to exist
let missing = find_user(2).unwrap(); // panics: called `Option::unwrap()` on a `None` value.expect("message") does the same but with a custom panic message, which makes debugging a crash much faster:
let config = std::env::var("API_KEY").expect("API_KEY must be set");Both are fine for prototypes, examples, and cases where a None/Err truly means a bug in your own program. In real application code handling untrusted input — user data, network responses, file contents — prefer match, if let, or the ? operator (covered in the next lesson) so a missing value becomes a handled case instead of a crash.
Useful combinators
Both types offer methods that avoid a full match for simple transformations:
fn main() {
let maybe_number: Option<i32> = Some(4);
let doubled = maybe_number.map(|n| n * 2); // Some(8)
let value = maybe_number.unwrap_or(0); // 4, or 0 if it had been None
let is_some = maybe_number.is_some(); // true
}Option and Result show up in almost every non-trivial Rust function signature you'll encounter — internalizing them now pays off throughout the rest of this course, especially the next lesson, which covers propagating errors up through a call chain with the ? operator.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.