Option & Result
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and it has no null and no exceptions. Instead, absence and failure are modelled with two enums: Option and Result.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll use Option<T> for maybe-missing values and Result<T, E> for operations that can fail, and handle both safely.
What You'll Learn in This Lesson
- Why Rust has no null
- Option<T> with Some and None
- Result<T, E> with Ok and Err
- Handling both with match
- Helpers like unwrap_or and is_some
- Why unwrap is risky
1️⃣ Option: Values That Might Be Missing
Option<T> is an enum with two variants: Some(value) when a value exists and None when it doesn't. Because it's a distinct type, the compiler forces you to deal with the None case before you can touch the value — eliminating null-pointer bugs.
fn main() {
// Option<T> models a value that MIGHT be absent. It has two variants:
// Some(value) — there is a value
// None — there is nothing
// Rust has no null; Option is how absence is expressed safely.
let numbers = vec![10, 20, 30];
// .get(index) returns Option<&i32>: Some(&v) if in range, None if not.
let found: Option<&i32> = numbers.get(1); // Some(&20)
let missing: Option<&i32> = numbers.get(99); // None
// match forces you to handle BOTH cases.
match found {
Some(v) => println!("found: {}", v), // -> found: 20
None => println!("nothing there"),
}
// Handy helpers:
println!("missing or default: {}", missing.unwrap_or(&-1)); // -> -1
println!("is some? {}", found.is_some()); // -> is some? true
}Methods like .get() return an Option precisely because the index might be out of range. Helpers such as unwrap_or let you supply a fallback without a full match.
2️⃣ Result: Operations That Can Fail
Result<T, E> is the failure-aware sibling of Option. It is Ok(value) on success or Err(error) on failure, where the error carries information about what went wrong. Functions that can fail — parsing, dividing, file I/O — return a Result.
// Result<T, E> models an operation that can FAIL:
// Ok(value) — success, carrying the value
// Err(error) — failure, carrying an error
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() {
// Handle a Result with match.
match divide(10.0, 2.0) {
Ok(value) => println!("10 / 2 = {}", value), // -> 10 / 2 = 5
Err(e) => println!("error: {}", e),
}
match divide(1.0, 0.0) {
Ok(value) => println!("result = {}", value),
Err(e) => println!("error: {}", e), // -> error: cannot divide by zero
}
// .parse() returns a Result. unwrap_or supplies a fallback on Err.
let n: i32 = "42".parse().unwrap_or(0);
let bad: i32 = "oops".parse().unwrap_or(0);
println!("parsed: {} and {}", n, bad); // -> parsed: 42 and 0
}The difference from Option: Err tells you why it failed. That's why divide returns a descriptive message instead of just None.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the Option and Result handling, then run it.
fn find_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return ___(n); // 👉 wrap n in the "has a value" variant
}
}
None // no even number found
}
fn safe_sqrt(x: f64) -> Result<f64, String> {
if x < 0.0 {
___(String::from("negative")) // 👉 the failure variant
} else {
Ok(x.sqrt())
}
}
fn main() {
match find_even(&[1, 3, 4, 7]) {
Some(v) => println!("first even: {}", v), // expected: first even: 4
None => println!("none found"),
}
println!("sqrt: {:?}", safe_sqrt(9.0)); // expected: sqrt: Ok(3.0)
}
// ✅ Expected output:
// first even: 4
// sqrt: Ok(3.0)Mini-Challenge: A Safe Lookup
Return an Option<char> and handle both cases. Run it with cargo run.
// 🎯 MINI-CHALLENGE: a safe lookup.
//
// 1. Write fn first_char(s: &str) -> Option<char> that returns Some(first
// character) or None for an empty string. Hint: s.chars().next() already
// returns an Option<char>.
// 2. In main, call it on "rust" and on "" (empty).
// 3. For each, use match (or unwrap_or) to print the result:
// - print "First char: r" when there is one
// - print "No characters" when empty
//
// ✅ Example output:
// First char: r
// No characters
fn main() {
// your code here
}Common Errors (and the fix)
- "called `Option::unwrap()` on a `None` value" — A runtime panic. Use match, if let, or unwrap_or to handle None safely.
- "mismatched types: expected `i32`, found `Option`" — You used an Option where a plain value was expected. Unwrap it safely first.
- "non-exhaustive patterns" — A match on an Option/Result must cover both variants.
- Returning a value where Result is expected — Wrap successes in Ok(...) and failures in Err(...).
Pro Tips
- 💡 Avoid unwrap in real code: prefer match, if let, or unwrap_or so a missing value can't crash you.
- 💡 Map without unwrapping: opt.map(|x| x * 2) transforms the inside while keeping it an Option.
- 💡 The ? operator is coming: it makes propagating Result errors effortless — that's the error-handling lesson.
📋 Quick Reference — Option & Result
| Goal | Rust Syntax |
|---|---|
| Has a value | Some(5) |
| No value | None |
| Success | Ok(5) |
| Failure | Err("oops") |
| Fallback | opt.unwrap_or(0) |
| Check presence | opt.is_some() |
🎉 Lesson Complete!
- ✅ Rust has no null; Option<T> models a maybe-missing value
- ✅ Result<T, E> models success (Ok) or failure (Err)
- ✅ match forces you to handle every case
- ✅ unwrap_or is a safe fallback; unwrap can panic
Practice quiz
What are the two variants of Option<T>?
- Ok and Err
- Just and Nothing
- Some and None
- True and False
Answer: Some and None. Option is either Some(value) or None.
Why does Rust use Option instead of null?
- The compiler forces you to handle the None case
- It is faster
- It uses less memory
- null is deprecated
Answer: The compiler forces you to handle the None case. Option is a distinct type, so missing values must be handled at compile time.
What does vec![10,20,30].get(99) return?
- 20
- Some(99)
- a panic
- None
Answer: None. .get on an out-of-range index returns None.
What does numbers.get(99).unwrap_or(&-1) return?
- 99
- -1
- None
- a panic
Answer: -1. unwrap_or supplies the fallback -1 when the value is None.
What are the two variants of Result<T, E>?
- Ok and Err
- Some and None
- Pass and Fail
- Yes and No
Answer: Ok and Err. Result is Ok(value) on success or Err(error) on failure.
What is the key difference between Option and Result?
- Option is faster
- Result has no None
- Result carries an error describing the failure
- There is no difference
Answer: Result carries an error describing the failure. Result's Err carries error info; Option's None carries none.
What does "oops".parse::<i32>().unwrap_or(0) return?
- oops
- 0
- None
- a panic
Answer: 0. parse fails, so unwrap_or returns the fallback 0.
Why is calling unwrap() risky?
- It is slow
- It returns null
- It never compiles
- It panics on None or Err
Answer: It panics on None or Err. unwrap panics and crashes the program on a None or Err.
What does Some(5).map(|x| x * 2) produce?
- 10
- Some(10)
- Some(5)
- None
Answer: Some(10). map transforms the inner value while keeping it wrapped as Some.
When should you prefer Result over Option?
- When absence is normal
- Always
- When you need to know why something failed
- Never
Answer: When you need to know why something failed. Use Result when you need the reason for a failure; Option for plain absence.
Continue this course
- Previous: Enums & Pattern Matching
- Next: Vectors — Store growable lists of values with Vec<T>
- Quick reference: Rust cheat sheet › Structs & Enums
Frequently asked questions
Why does Rust use Option instead of null?
Null references are a famous source of crashes because the type system does not force you to check for them — you can accidentally use a null and the program blows up at runtime. Rust has no null. Instead, a value that might be absent has type Option<T>, which is either Some(value) or None. Because Option is a distinct type, the compiler makes you handle the None case before you can get at the value, so 'forgot to check for missing' becomes a compile error rather than a runtime crash.
What is the difference between Option and Result?
Option<T> represents the presence or absence of a value: Some(value) or None. It answers 'is there something here?'. Result<T, E> represents success or failure of an operation that can go wrong: Ok(value) or Err(error). The key difference is that Result's failure case carries an error describing what went wrong, while Option's None carries no information. Use Option when absence is normal and expected; use Result when you need to know why something failed.
What does unwrap do, and why is it risky?
unwrap extracts the inner value from a Some or an Ok. But if you call it on a None or an Err, it panics and crashes the program. That makes it convenient for quick prototypes, examples, and tests where you are certain a value is present, but dangerous in real code where the absence or error is possible. Safer alternatives include unwrap_or(default), match, if let, and the ? operator, which you will learn in the error-handling lesson.
What does unwrap_or do?
unwrap_or(default) returns the inner value when you have Some or Ok, and returns the default you provide when you have None or Err — without ever panicking. It is a clean way to supply a fallback, such as 'value.parse().unwrap_or(0)' to default a failed parse to zero. There are related methods too: unwrap_or_else takes a closure that computes the default lazily, and unwrap_or_default uses the type's standard default value.