Working Code
Other languages use null when a value doesn't exist — and then suffer from the infamous NullPointerException. Rust has no null. Instead, Option<T> expresses "a value may or may not exist."
fn main() {
let fruits = vec!["apple", "banana", "grape"];
let first = fruits.get(0); // Some("apple")
let fourth = fruits.get(10); // None
println!("First: {:?}", first);
println!("Fourth: {:?}", fourth);
}
vec.get() returns Option<&T>. If the index is in range, you get Some(value); otherwise, None. Unlike fruits[10] which panics, .get(10) safely returns None.
Try It Yourself
- Print the result of
fruits.get(2). - Try
fruits[10]instead. What error do you see? - Compare it with
fruits.get(10). Can you see whyNoneis better than a panic?
"Why?" — Think of It Like an Inventory Check
Think of Option as an inventory check at a store:
Some(item)— In stock! Here's the itemNone— Out of stock! Nothing here
enum Option<T> {
Some(T), // value exists
None, // no value
}
Option<T> is just an enum from Lesson 13! It's defined in the standard library, and Some and None are available without any imports.
Handling With match
The most fundamental approach. Since match must be exhaustive, it's inherently safe:
fn find_discount(item: &str) -> Option<u32> {
match item {
"apple" => Some(10),
"banana" => Some(20),
_ => None,
}
}
fn main() {
let item = "apple";
match find_discount(item) {
Some(percent) => println!("{item}: {percent}% off!"),
None => println!("{item}: no discount"),
}
}
if let — When You Only Care About Some
If you only want to handle the Some case, if let is more concise:
fn main() {
let name: Option<&str> = Some("Alice");
if let Some(n) = name {
println!("Hello, {n}!");
}
// If None, nothing happens
}
unwrap_or() — Provide a Default
Specify a fallback value for the None case:
fn main() {
let port: Option<u16> = None;
let actual_port = port.unwrap_or(8080);
println!("Port: {actual_port}"); // Port: 8080
let custom: Option<u16> = Some(3000);
let actual = custom.unwrap_or(8080);
println!("Port: {actual}"); // Port: 3000
}
There's also unwrap_or_default(), which uses the type's default value (0, "", false, etc.):
fn main() {
let count: Option<i32> = None;
println!("Count: {}", count.unwrap_or_default()); // Count: 0
let name: Option<String> = None;
println!("Name: '{}'", name.unwrap_or_default()); // Name: ''
}
map() — Transform Only When Present
Use map() to transform the value inside Some. If it's None, it stays None:
fn main() {
let name: Option<&str> = Some("rust");
let upper = name.map(|n| n.to_uppercase());
println!("{:?}", upper); // Some("RUST")
let empty: Option<&str> = None;
let upper = empty.map(|n| n.to_uppercase());
println!("{:?}", upper); // None
}
and_then() — Chaining When the Transform Returns Option
Use map() when the transform returns a plain value. Use and_then() when the transform itself returns an Option:
fn parse_port(s: &str) -> Option<u16> {
s.parse::<u16>().ok()
}
fn main() {
let input: Option<&str> = Some("8080");
let port = input.and_then(parse_port);
println!("{:?}", port); // Some(8080)
let bad: Option<&str> = Some("abc");
let port = bad.and_then(parse_port);
println!("{:?}", port); // None
}
Using map() here would produce Option<Option<u16>>. and_then() flattens it to Option<u16>.
Real Example: User Lookup
Combining multiple methods leads to clean code:
struct User {
name: String,
email: Option<String>,
}
fn find_user(users: &[User], name: &str) -> Option<&User> {
users.iter().find(|u| u.name == name)
}
fn main() {
let users = vec![
User { name: "Alice".into(), email: Some("alice@example.com".into()) },
User { name: "Bob".into(), email: None },
];
// Find user, then get email if it exists
let email = find_user(&users, "Alice")
.and_then(|u| u.email.as_deref())
.unwrap_or("no email");
println!("Alice's email: {email}");
let email = find_user(&users, "Carol")
.and_then(|u| u.email.as_deref())
.unwrap_or("no email");
println!("Carol's email: {email}");
}
Why Is unwrap() Dangerous?
unwrap() panics on None. In Lesson 16 you learned that Result's unwrap() is dangerous — Option is the same:
fn main() {
let value: Option<i32> = None;
// value.unwrap(); // panic: called `Option::unwrap()` on a `None` value
}
| Method | On None | Use case |
| --------------------- | -------------------- | ------------------------------------ |
| unwrap() | panic! | Test code, guaranteed-Some scenarios |
| unwrap_or(default) | Returns default | When you have a fallback value |
| unwrap_or_default() | Returns type default | When 0, "", etc. are acceptable |
| expect("message") | Panic with message | Debugging |
As you get comfortable, chaining map(), and_then(), and unwrap_or() becomes second nature. It feels awkward at first, but reading Rust code will make it click!
Exercise 1 (easy). Complete the function. It finds and returns the first even number in a vector.
fn first_even(numbers: &[i32]) -> Option<i32> {
// Complete this
// Hint: use a for loop and if to find an even number, return Some(n)
// Return None if not found
}
fn main() {
let nums = vec![1, 3, 4, 7, 8];
println!("{:?}", first_even(&nums)); // Some(4)
let odds = vec![1, 3, 5];
println!("{:?}", first_even(&odds)); // None
}
Exercise 2 (medium). Remove the unwrap() calls and replace them with safe alternatives.
fn main() {
let config = vec![
("host", "localhost"),
("port", "8080"),
];
// Fix these without unwrap()!
let host = config.iter()
.find(|(k, _)| *k == "host")
.unwrap()
.1;
let timeout = config.iter()
.find(|(k, _)| *k == "timeout")
.unwrap() // panic here!
.1;
println!("host: {host}, timeout: {timeout}");
}
Hint: Combine map() and unwrap_or().
Exercise 3 (challenge). Use and_then() to handle nested Option values.
fn get_area_code(phone: Option<&str>) -> Option<&str> {
// If phone is Some and contains "-", return the part before "-" (area code)
// Otherwise return None
// Hint: use and_then() and split_once()
}
fn main() {
println!("{:?}", get_area_code(Some("02-1234-5678"))); // Some("02")
println!("{:?}", get_area_code(Some("01012345678"))); // None
println!("{:?}", get_area_code(None)); // None
}
Q1. Which statement about Option<T> is correct?
- A) It returns null when there's no value
- B) It uses Some(T) and None to indicate presence or absence of a value
- C) It terminates the program when an error occurs
- D) It is exactly the same as Result<T, E>
Q2. What does this code print?
fn main() {
let v = vec![10, 20, 30];
let value = v.get(5).unwrap_or(&0);
println!("{value}");
}
- A) 30
- B) None
- C) 0
- D) The program panics
Q3. What is the difference between map() and and_then()?
- A) No difference
- B)
map()is for transforms returning plain values;and_then()is for transforms returning Option - C)
map()only works on None - D)
and_then()always causes a panic