Option (instead of null)
Data · Lesson 23
Rust has no nil or null. A value that might be missing has the type Option<T>, which is either Some(value) or None.
Coming from GoOption<User> replaces a *User that might be nil, and also the (value, ok) pair from a map lookup: one value carries both. You can't reach .name until you've checked for Some, so you can't dereference a missing value by accident.
Coming from KotlinOption<User> is your User?, and for Kotlin code the compiler checks it just as strictly. The operators become methods: ?. is .map(...) or .and_then(...), ?: is .unwrap_or_else(...) (both are lazy), and !! is .unwrap(). Option is an ordinary type rather than a mark on a type, so it nests (Option<Option<T>>) and has methods, and no value arrives from Java with unknown nullness.
Coming from PythonOption<User> is your Optional[User], but the check isn't left to a tool you may forget to run: the compiler won't let you touch .name until you've matched Some. That removes the unchecked AttributeError on None.
Coming from TypeScriptOption<User> is User | undefined with strictNullChecks always on. Rust has one "nothing", None, not both null and undefined. There is no ?. or ??: you use methods like .map(...) and .unwrap_or(...), or match and if let.
Coming from JavaOption<T> is Optional<T>, but in Rust it is the standard way to say a value may be missing, and there is no null for an Option itself to be. Java still has null for every reference, including an Optional variable. match or if let do what ifPresentOrElse and map(...).orElse(...) do.
Coming from C#Option<User> is what User? means, but Rust enforces it: you cannot touch .name until you've checked for Some. With nullable reference types, C# reports the same mistake (CS8602) as a warning by default, and the program still builds.
Coming from COption<&User> replaces a pointer that might be NULL, and it is stored as just that pointer, with None as the null address. The difference is that you must check for None before using it; you can't forget and read address 0.
Coming from C++Option<T> is std::optional<T>. if (auto u = f()) tests the optional, but u is still an optional and u->name skips the test; Rust has no such shortcut, and unwrap() panics with a message on None instead of being undefined behaviour. Option<&T> is what std::optional<T&> is in C++26, and what a raw pointer was before.
The quick comparison
struct User { name: String,}
fn find_user(id: u32) -> Option<User> { if id == 1 { Some(User { name: String::from("ana"), }) } else { None }}
fn main() { match find_user(1) { Some(u) => println!("found {}", u.name), None => println!("no user"), }
// if let: the short form when you only care about Some if let Some(u) = find_user(2) { println!("found {}", u.name); } else { println!("no user"); }}found ana no user
package main
import "fmt"
type User struct { Name string}
func findUser(id int) *User { if id == 1 { return &User{Name: "ana"} } return nil}
func main() { if u := findUser(1); u != nil { fmt.Println("found", u.Name) } else { fmt.Println("no user") }
if u := findUser(2); u != nil { fmt.Println("found", u.Name) } else { fmt.Println("no user") }}found ana no user
class User(val name: String)
fun findUser(id: Int): User? = if (id == 1) User("ana") else null
fun main() { val a = findUser(1) if (a != null) println("found ${a.name}") else println("no user")
val b = findUser(2) println(if (b != null) "found ${b.name}" else "no user")}found ana no user
from dataclasses import dataclass
@dataclassclass User: name: str
def find_user(id: int) -> User | None: # None plays the role of Option's None if id == 1: return User("ana") return None
u = find_user(1)if u is not None: print(f"found {u.name}")else: print("no user")
if (u := find_user(2)) is not None: print(f"found {u.name}")else: print("no user")found ana no user
interface User { name: string;}
// with strictNullChecks, `undefined` must be part of the type to be returnedfunction findUser(id: number): User | undefined { return id === 1 ? { name: "ana" } : undefined;}
const u = findUser(1);if (u !== undefined) { console.log(`found ${u.name}`);} else { console.log("no user");}
// the short form: ?. gives undefined when there is no userconst found = findUser(2)?.name;console.log(found ? `found ${found}` : "no user");found ana no user
import java.util.Optional;
public class Main { record User(String name) {}
static Optional<User> findUser(int id) { return id == 1 ? Optional.of(new User("ana")) : Optional.empty(); }
public static void main(String[] args) { findUser(1).ifPresentOrElse( u -> System.out.println("found " + u.name()), () -> System.out.println("no user"));
// map + orElse: the short form System.out.println(findUser(2).map(u -> "found " + u.name()).orElse("no user")); }}found ana no user
var found = FindUser(1);if (found is not null){ Console.WriteLine($"found {found.Name}");}else{ Console.WriteLine("no user");}
// pattern matching does the check and the unwrap in one stepif (FindUser(2) is { } u){ Console.WriteLine($"found {u.Name}");}else{ Console.WriteLine("no user");}
// User? means "a User, or null"static User? FindUser(uint id) => id == 1 ? new User("ana") : null;
record User(string Name);found ana no user
#include <stdio.h>
struct User { const char *name;};
static struct User ana = {"ana"};
// C has no Option: return a pointer, and NULL means "none"const struct User *find_user(unsigned id) { if (id == 1) { return &ana; } return NULL;}
int main(void) { const struct User *u = find_user(1); if (u != NULL) { printf("found %s\n", u->name); } else { printf("no user\n"); }
u = find_user(2); if (u != NULL) { printf("found %s\n", u->name); } else { printf("no user\n"); } return 0;}found ana no user
#include <iostream>#include <optional>#include <string>
struct User { std::string name;};
std::optional<User> find_user(int id) { if (id == 1) { return User{"ana"}; } return std::nullopt;}
int main() { if (auto u = find_user(1)) { // true only when it holds a value std::cout << "found " << u->name << "\n"; } else { std::cout << "no user\n"; }
if (auto u = find_user(2)) { std::cout << "found " << u->name << "\n"; } else { std::cout << "no user\n"; }}found ana no user
What's different
Option<User>is eitherSome(user)orNone. In safe Rust a reference is never null, so "might be missing" is spelled out as a type you can see.- Handy shortcuts:
opt.unwrap_or(default)gives a fallback value,opt.map(|u| u.name)changes the value inside if there is one (|u| u.nameis a closure, a small function written inline; Closures come later), andopt?inside a function that returns Option hands back None early. opt.unwrap()takes the value out and crashes if it is None. Use it only where None really cannot happen.- Missing for a reason, not by accident: if a function can fail with a reason, return a
Resultinstead (next lesson).Optionsays "nothing";Resultsays why.
Using the value without checking
Looks fine: in Go, Python, Java, C# and C it crashes at runtime. Rust won't compile it.
Broken in Go. Rust catches it.Kotlin refuses this too. Same idea, different words.Broken in Python. Rust catches it.TypeScript refuses this too. Same idea, different words.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.C++ refuses this too. Same idea, different words.struct User { name: String,}
fn find_user(id: u32) -> Option<User> { if id == 1 { Some(User { name: String::from("ana"), }) } else { None }}
fn main() { let u = find_user(2); println!("{}", u.name);}package main
import "fmt"
type User struct{ Name string }
func findUser(id int) *User { if id == 1 { return &User{Name: "ana"} } return nil}
func main() { u := findUser(2) fmt.Println(u.Name)}panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xa1ff0] goroutine 1 [running]: main.main() main.go:16 +0x20
Kotlin refuses this too: findUser returns User?, so u.name needs u?.name or a null check first.
class User(val name: String)
fun findUser(id: Int): User? = if (id == 1) User("ana") else null
fun main() { val u = findUser(2) println(u.name)}Main.kt:8:14: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'User?'.
println(u.name)
^from dataclasses import dataclass
@dataclassclass User: name: str
def find_user(id: int) -> User | None: return User("ana") if id == 1 else None
u = find_user(2)print(u.name)Traceback (most recent call last):
File "main.py", line 14, in <module>
print(u.name)
^^^^^^
AttributeError: 'NoneType' object has no attribute 'name'TypeScript refuses this too with strict on: u is possibly undefined, so you must check it before reading u.name.
interface User { name: string;}
function findUser(id: number): User | undefined { return id === 1 ? { name: "ana" } : undefined;}
const u = findUser(2);console.log(u.name);main.ts(10,13): error TS18048: 'u' is possibly 'undefined'.
public class Main { record User(String name) {}
static User findUser(int id) { return id == 1 ? new User("ana") : null; }
public static void main(String[] args) { var u = findUser(2); System.out.println(u.name()); }}Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Main$User.name()" because "<local1>" is null at Main.main(Main.java:10)
var u = FindUser(2);Console.WriteLine(u.Name);
static User? FindUser(uint id) => id == 1 ? new User("ana") : null;
record User(string Name);Program.cs(2,19): warning CS8602: Dereference of a possibly null reference.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in Program.cs:line 2
#include <stdio.h>
struct User { const char *name;};
static struct User ana = {"ana"};
struct User *find_user(unsigned id) { if (id == 1) return &ana; return NULL;}
int main(void) { struct User *u = find_user(2); printf("%s\n", u->name); return 0;}AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x············ (pc 0x············ bp 0x············ sp 0x············ T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x············ in main main.c:16
#1 0x············ (/lib/aarch64-linux-gnu/libc.so.6+0x22258)
#2 0x············ in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x22338)
#3 0x············ in _start (main+0xa2c)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV main.c:16 in main
==1==ABORTINGC++ refuses this too: std::optional<User> has no member name. Be careful though, because u->name would compile and skip the check, which is undefined behaviour when u is empty.
#include <iostream>#include <optional>#include <string>
struct User { std::string name;};
std::optional<User> find_user(int id) { if (id == 1) return User{"ana"}; return std::nullopt;}
int main() { auto u = find_user(2); std::cout << u.name << "\n";}main.cpp: In function ‘int main()’:
main.cpp:16:20: error: ‘class std::optional<User>’ has no member named ‘name’
16 | std::cout << u.name << "\n";
| ^~~~What does the compiler say? Guess first, then open.
error[E0609]: no field `name` on type `Option<User>` --> src/main.rs:17:22 | 17 | println!("{}", u.name); | ^^^^ unknown field | help: one of the expressions' fields has a field of the same name | 17 | println!("{}", u.unwrap().name); | +++++++++
Go compiles this, then crashes with a nil pointer panic when it reads u.Name.
Kotlin refuses this too: findUser returns User?, so u.name needs u?.name or a null check first.
Python runs until u.name, then raises AttributeError because u is None. This is the same crash as None.name anywhere. A type checker such as mypy or pyright flags it first, but only if you run one, and # type: ignore, cast and Any can switch the check off.
TypeScript refuses this too with strict on: u is possibly undefined, so you must check it before reading u.name.
It compiles, then throws a NullPointerException because findUser(2) returned null. Nothing made you check first.
With nullable references on, the compiler warns about the possible null (CS8602). It is only a warning by default, so the program builds and then throws a NullReferenceException.
C compiles this without a word. find_user(2) returns NULL, and AddressSanitizer reports a crash reading address 0.
C++ refuses this too: std::optional<User> has no member name. Be careful though, because u->name would compile and skip the check, which is undefined behaviour when u is empty.
u is an Option<User>, not a User. An Option has no name field. You have to open it first.
Rust moves the nil check from "remember to do it" to "can't compile without it". Every place that might get nothing has to say what happens then. The compiler's help: suggests u.unwrap().name. That compiles, but unwrap on None crashes, which is the classic null crash all over again. Use match or if let instead.
The fix
Compiles and runs.
struct User { name: String,}
fn find_user(id: u32) -> Option<User> { if id == 1 { Some(User { name: String::from("ana"), }) } else { None }}
fn main() { let u = find_user(2); match u { Some(user) => println!("{}", user.name), None => println!("no user"), }}no user
The toolkit
Rust has no null, nil or undefined. A value that might be missing gets its own type, Option<T>, and the compiler makes you handle the missing case before you can use the value. Outputs on this page come from Linux on 64-bit ARM with Rust 1.99.1; where a fact depends on the platform, the text says so.
What Option is
Option<T> is an enum, a type that is exactly one of a fixed list of named forms (Enums covers them). It has two:
Some(value): there is a value, and it is inside.None: there is no value.
Option is not special syntax. It is an ordinary enum from the standard library, and Some and None are in scope without an import. Only the ? operator, below, gives it a shorthand of its own. The T stands for the type inside: Option<u32>, Option<String>, Option<User>.
Why no null? Where any reference can be null, the type User means “a user, or maybe nothing”, and nothing in the type says which. Every use is a place to forget the check. Tony Hoare, who is credited with inventing null references, later called it his “billion-dollar mistake” (The Rust Book quotes him). Kotlin, TypeScript and C# add a compile-time check on top of a nullable type and get much of the benefit; how they differ is below. Rust moves the idea into the type itself. A User is a user. One that might be missing is an Option<User>, a different type.
Getting the value out
The first tool is match. The compiler checks that you wrote an arm for both Some and None. This program looks up three users. Each User holds an Option for its email, and a pattern can reach inside both. (&'static str is the type of a string literal such as "ana"; Strings covers text.)
struct User { name: &'static str, email: Option<&'static str>, age: Option<u32>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { name: "ana", age: Some(31), }), 2 => Some(User { name: "bo", email: None, age: None, }), _ => None, }}
fn main() { for id in [1, 2, 3] { match find_user(id) { Some(User { name, email: Some(email), age, }) => { println!("id {id}: {name}, email {email}, age {age:?}"); } Some(User { name, email: None, age, }) => { println!("id {id}: {name}, no email, age {age:?}"); } None => println!("id {id}: no such user"), } }}id 1: ana, email [email protected], age Some(31) id 2: bo, no email, age None id 3: no such user
Leave out None and the program does not compile:
fn first_even(numbers: [i32; 3]) -> Option<i32> { for n in numbers { if n % 2 == 0 { return Some(n); } } None}
fn main() { match first_even([1, 3, 5]) { Some(n) => println!("first even: {n}"), }}error[E0004]: non-exhaustive patterns: `None` not covered --> src/main.rs:11:11 | 11 | match first_even([1, 3, 5]) { | ^^^^^^^^^^^^^^^^^^^^^ pattern `None` not covered | help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 12 ~ Some(n) => println!("first even: {n}"), 13 ~ None => todo!(), ... (notes pointing into the standard library trimmed)
E0004, a non-exhaustive match. The message names the missing case and the help: shows the arm to add. The “forgot the null check” bug cannot get into a build by accident.
Two shorter forms cover most code:
if let Some(x) = opt { … }runs the block only forSome. Anelseis optional.let Some(x) = opt else { … };unpacks the value, or runs theelseblock, which must leave (return,break,continueor a panic). The code after it keeps its normal indentation.
struct User { name: &'static str, age: Option<u32>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { name: "ana", age: Some(31), }), 2 => Some(User { name: "bo", age: None, }), _ => None, }}
// if let: run some code only for Some, and ignore None.fn print_age(id: u32) { if let Some(User { age: Some(age), .. }) = find_user(id) { println!("id {id}: age {age}"); } else { println!("id {id}: age unknown"); }}
// let else: unwrap or leave. The happy path stays unindented.fn greeting(id: u32) -> String { let Some(user) = find_user(id) else { return format!("id {id}: nobody home"); }; format!("id {id}: hello, {}", user.name)}
fn main() { print_age(1); print_age(2); print_age(3); println!("{}", greeting(1)); println!("{}", greeting(3));}id 1: age 31 id 2: age unknown id 3: age unknown id 1: hello, ana id 3: nobody home
Use if let when None means “do nothing”, and let else when it means “stop here”.
while let repeats while a pattern matches. pop removes the last item of a Vec (a growable list, covered in Vectors) and returns Some(item), or None when it is empty:
fn main() { let mut stack = vec![1, 2, 3];
// pop() returns Some(last item), or None once the list is empty. while let Some(top) = stack.pop() { println!("popped {top}"); } println!("left over: {stack:?}"); println!("pop on empty: {:?}", stack.pop());}popped 3 popped 2 popped 1 left over: [] pop on empty: None
You can’t use an Option as the value
A u32 and an Option<u32> are different types, so the compiler stops you mixing them:
fn main() { let age: Option<u32> = Some(30); let next_year = age + 1; println!("next year: {next_year}");}error[E0369]: cannot add `{integer}` to `Option<u32>` --> src/main.rs:3:25 | 3 | let next_year = age + 1; | --- ^ - {integer} | | | Option<u32> ... (notes pointing into the standard library trimmed)
E0369: Option<u32> has no +. The same mistake in a typed let or a function argument is E0308, mismatched types:
fn find_age(id: u32) -> Option<u32> { if id == 1 { Some(31) } else { None }}
fn main() { let age: u32 = find_age(1); println!("age: {age}");}error[E0308]: mismatched types --> src/main.rs:6:20 | 6 | let age: u32 = find_age(1); | --- ^^^^^^^^^^^ expected `u32`, found `Option<u32>` | | | expected due to this | = note: expected type `u32` found enum `Option<u32>` help: consider using `Option::expect` to unwrap the `Option<u32>` value, panicking if the value is an `Option::None` | 6 | let age: u32 = find_age(1).expect("REASON"); | +++++++++++++++++
The help: suggests .expect("REASON"), which compiles but turns None into a crash. Look at the methods below first.
The four you will use most
map, and_then, unwrap_or and ? do most of the work. Everything else is further down to look up when you meet it.
A closure is a small function written inline: |n| n * 2 takes n and gives n * 2 (n => n * 2 in TypeScript or C#, lambda n: n * 2 in Python, { n -> n * 2 } in Kotlin, n -> n * 2 in Java). || 5 takes nothing. Closures come later.
map and and_then
map changes the value inside a Some and leaves None alone. and_then is for a function that itself returns an Option: map would leave an Option inside an Option, and and_then flattens that into one.
struct User { name: &'static str, email: Option<&'static str>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { name: "ana", }), 2 => Some(User { name: "bo", email: None, }), _ => None, }}
fn main() { // map: change the value inside Some. None stays None. println!( "name length, user 1: {:?}", find_user(1).map(|u| u.name.len()) ); println!( "name length, user 3: {:?}", find_user(3).map(|u| u.name.len()) );
// map with a function that itself returns an Option nests them. println!("map to email, user 1: {:?}", find_user(1).map(|u| u.email)); println!("map to email, user 2: {:?}", find_user(2).map(|u| u.email));
// and_then flattens: one Option out, not two. println!( "and_then email, user 1: {:?}", find_user(1).and_then(|u| u.email) ); println!( "and_then email, user 2: {:?}", find_user(2).and_then(|u| u.email) );
// A chain of lookups: any step that gives None ends it with None. for id in [1, 2, 3] { let domain = find_user(id) .and_then(|u| u.email) .and_then(|email| email.split_once('@')) .map(|(_, domain)| domain); println!("domain, user {id}: {domain:?}"); }}name length, user 1: Some(3)
name length, user 3: None
map to email, user 1: Some(Some("[email protected]"))
map to email, user 2: Some(None)
and_then email, user 1: Some("[email protected]")
and_then email, user 2: None
domain, user 1: Some("example.com")
domain, user 2: None
domain, user 3: NoneRead the last loop top to bottom: find the user, take the email, split it at @, keep the domain. Any step can give None, and then the rest are skipped.
unwrap_or and friends
These end the Option and give you the plain value, with a replacement for None:
unwrap_or(x)takes a ready-made value.unwrap_or_else(|| …)takes a function and calls it only when needed.unwrap_or_default()uses the type’s own default:0, an empty string, an empty list.
fn slow_default(who: &str) -> u32 { println!(" ({who} ran the default code)"); 18}
fn main() { let known: Option<u32> = Some(40); let missing: Option<u32> = None;
println!("unwrap_or on Some:"); let a = known.unwrap_or(slow_default("unwrap_or")); println!(" result: {a}");
println!("unwrap_or_else on Some:"); let b = known.unwrap_or_else(|| slow_default("unwrap_or_else")); println!(" result: {b}");
println!("unwrap_or_else on None:"); let c = missing.unwrap_or_else(|| slow_default("unwrap_or_else")); println!(" result: {c}");
// unwrap_or_default uses the type's own default: 0, "", false, an empty list... let name: Option<String> = None; let nums: Option<Vec<i32>> = None; println!("number default: {}", missing.unwrap_or_default()); println!("text default: {:?}", name.unwrap_or_default()); println!("list default: {:?}", nums.unwrap_or_default());}unwrap_or on Some: (unwrap_or ran the default code) result: 40 unwrap_or_else on Some: result: 40 unwrap_or_else on None: (unwrap_or_else ran the default code) result: 18 number default: 0 text default: "" list default: []
The first call shows why _else exists: unwrap_or(slow_default(…)) runs slow_default before unwrap_or is called, even though the value was Some. Kotlin’s ?: and C#’s ?? are lazy like unwrap_or_else, not like unwrap_or.
?
Inside a function that returns an Option, a ? after an Option means: if it is Some, unwrap it; if it is None, return None from the whole function now. A chain of and_then calls becomes plain steps:
struct User { email: Option<&'static str>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { }), 2 => Some(User { email: None }), _ => None, }}
// Each ? means: if this is None, return None from the whole function now.fn email_domain(id: u32) -> Option<&'static str> { let user = find_user(id)?; let email = user.email?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { for id in [1, 2, 3] { println!("domain, user {id}: {:?}", email_domain(id)); }}domain, user 1: Some("example.com")
domain, user 2: None
domain, user 3: None? works only where the function’s own return type can carry the None:
fn first_even(numbers: [i32; 3]) -> Option<i32> { numbers.into_iter().find(|n| n % 2 == 0)}
fn main() { let n = first_even([1, 2, 3])?; println!("first even: {n}");}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:6:34 | 5 | fn main() { | --------- this function should return `Result` or `Option` to accept `?` 6 | let n = first_even([1, 2, 3])?; | ^ cannot use the `?` operator in a function that returns `()`
E0277. main returns (), so there is nowhere to send a None. On a Result, ? does the same for errors (next lesson).
Your language, in Rust
Pick your language in “Coming from” at the top of the page to see how the code you know maps to Rust. The Rust cells that need care (moves, laziness, get, ok) are run by check-rows.rs.
The rest, when you need them
Every call here is checked by check-methods.rs, with some standing for Some(4) and none for None::<i32>. They all work because Option<i32> is Copy: a method that takes the Option by value copies it, so some is still usable afterwards. An Option<String> is not Copy, and the section on as_ref below is about that.
| Method | Call | Result |
|---|---|---|
filter |
some.filter(|n| n % 2 == 1) |
None |
zip |
some.zip(Some("hi")) |
Some((4, "hi")) |
map_or |
some.map_or(0, |n| n + 1) |
5 |
ok_or |
none.ok_or("missing") |
Err("missing") |
or |
none.or(Some(9)) |
Some(9) |
is_some_and |
some.is_some_and(|n| n > 3) |
true |
flatten |
Some(Some(3)).flatten() |
Some(3) |
unwrap_or |
Some(0).unwrap_or(5) |
0 |
filter keeps the value only if a test passes (the test receives a reference, so a number is written *age; the * is from Borrowing). zip pairs two options, and gives None if either is missing:
struct User { email: Option<&'static str>, age: Option<u32>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { age: Some(31), }), 2 => Some(User { email: None, age: Some(15), }), _ => None, }}
fn main() { // filter: keep the value only if the test says yes. let adult_1 = find_user(1).and_then(|u| u.age).filter(|age| *age >= 18); let adult_2 = find_user(2).and_then(|u| u.age).filter(|age| *age >= 18); println!("adult age, user 1: {adult_1:?}"); println!("adult age, user 2: {adult_2:?}");
// zip: Some of both, or None if either is missing. println!("zip, 1 and 'a': {:?}", Some(1).zip(Some("a"))); println!("zip, 1 and nothing: {:?}", Some(1).zip(None::<&str>)); if let Some(user) = find_user(1) { println!("age and email, user 1: {:?}", user.age.zip(user.email)); } if let Some(user) = find_user(2) { println!("age and email, user 2: {:?}", user.age.zip(user.email)); }}adult age, user 1: Some(31) adult age, user 2: None zip, 1 and 'a': Some((1, "a")) zip, 1 and nothing: None age and email, user 1: Some((31, "[email protected]")) age and email, user 2: None
or falls back to a second Option. ok_or turns None into an Err, so the caller gets a reason; Result is the type for “a value, or an error” in the next lesson. or_else and ok_or_else build the replacement only when needed:
struct User { name: &'static str, email: Option<&'static str>,}
fn find_user(id: u32) -> Option<User> { match id { 1 => Some(User { name: "ana", }), 2 => Some(User { name: "bo", email: None, }), _ => None, }}
fn main() { // ok_or: turn None into an error value, so the caller gets a reason. let found: Result<User, &str> = find_user(1).ok_or("no such user"); let lost: Result<User, &str> = find_user(3).ok_or("no such user"); println!("ok_or, user 1: {:?}", found.map(|u| u.name)); println!("ok_or, user 3: {:?}", lost.map(|u| u.name));
// ok_or_else builds the error only if it is needed. let id = 9; let err = find_user(id) .map(|u| u.name) .ok_or_else(|| format!("no user with id {id}")); println!("ok_or_else, user 9: {err:?}");
// or: use this Option, else that one. or_else only builds the other when needed. let own = |id: u32| find_user(id).and_then(|u| u.email); println!("or, user 1: {:?}", own(1).or(fallback)); println!("or, user 2: {:?}", own(2).or(fallback)); println!( "or_else, user 2: {:?}", );}ok_or, user 1: Ok("ana")
ok_or, user 3: Err("no such user")
ok_or_else, user 9: Err("no user with id 9")
or, user 1: Some("[email protected]")
or, user 2: Some("[email protected]")
or_else, user 2: Some("[email protected]")is_some, is_none and is_some_and return a bool. If you will use the value afterwards, if let is better: if opt.is_some() { opt.unwrap() } checks twice and brings the crash back. is_some_and takes the Option by value, like map, so for a String borrow first:
fn find_age(id: u32) -> Option<u32> { match id { 1 => Some(31), 2 => Some(15), _ => None, }}
fn main() { for id in [1, 2, 3] { let age = find_age(id); println!( "id {id}: is_some {}, is_none {}, is_some_and(adult) {}", age.is_some(), age.is_none(), age.is_some_and(|a| a >= 18), ); }
// is_some_and takes the Option by value, like map. For a String, borrow first. let name: Option<String> = Some(String::from("ana")); println!("long name: {}", name.as_ref().is_some_and(|n| n.len() > 5)); println!("name still there: {name:?}");}id 1: is_some true, is_none false, is_some_and(adult) true
id 2: is_some true, is_none false, is_some_and(adult) false
id 3: is_some false, is_none true, is_some_and(adult) false
long name: false
name still there: Some("ana")Looking without taking: as_ref, as_mut, as_deref, copied, cloned
map takes the Option by value, so an Option<String> is moved into it and gone afterwards:
fn main() { let nickname: Option<String> = Some(String::from("bo")); let length = nickname.map(|n| n.len()); println!("length: {length:?}"); println!("nickname: {nickname:?}");}error[E0382]: borrow of moved value: `nickname` --> src/main.rs:5:26 | 2 | let nickname: Option<String> = Some(String::from("bo")); | -------- move occurs because `nickname` has type `Option<String>`, which does not implement the `Copy` trait 3 | let length = nickname.map(|n| n.len()); | ---------------- `nickname` moved due to this method call 4 | println!("length: {length:?}"); 5 | println!("nickname: {nickname:?}"); | ^^^^^^^^ value borrowed here after move | help: consider calling `.as_ref()` to borrow the value's contents | 3 | let length = nickname.as_ref().map(|n| n.len()); | +++++++++ help: consider calling `.as_mut()` to mutably borrow the value's contents | 3 | let length = nickname.as_mut().map(|n| n.len()); | +++++++++ help: you can `clone` the value and consume it, but this might not be your desired behavior | 3 | let length = nickname.clone().map(|n| n.len()); | ++++++++ ... (notes pointing into the standard library trimmed)
E0382, a use after a move. The help: names the fix: as_ref() turns &Option<String> into an Option<&String>, a view that borrows the text (Borrowing), so the original stays put.
| Method | Turns | Use it to |
|---|---|---|
as_ref() |
Option<T> into Option<&T> |
read inside without moving |
as_mut() |
Option<T> into Option<&mut T> |
change inside, in place (Mutable borrows) |
as_deref() |
Option<String> into Option<&str> |
get the plain borrowed form |
copied() |
Option<&T> into Option<T> |
copy out a small value such as a number |
cloned() |
Option<&T> into Option<T> |
clone out a value that is not Copy, like a String |
fn main() { let mut nickname: Option<String> = Some(String::from("bo"));
// as_ref: Option<&String>, so the String stays where it is. let length = nickname.as_ref().map(|n| n.len()); println!("length: {length:?}"); println!("nickname still there: {nickname:?}");
// as_mut: Option<&mut String>, to change it in place. if let Some(n) = nickname.as_mut() { n.push_str("bby"); } println!("after as_mut: {nickname:?}");
// as_deref: Option<String> becomes Option<&str>, handy for a fallback. let shown: &str = nickname.as_deref().unwrap_or("anonymous"); let none: Option<String> = None; let shown_none: &str = none.as_deref().unwrap_or("anonymous"); println!("as_deref: {shown} and {shown_none}");
// get and first return Option<&T>. copied and cloned make an owned value. let scores = [10, 20, 30]; println!("get(1): {:?}", scores.get(1)); println!("get(1).copied(): {:?}", scores.get(1).copied()); println!("get(7).copied(): {:?}", scores.get(7).copied()); let names = [String::from("ana"), String::from("bo")]; let first: Option<String> = names.first().cloned(); println!("first().cloned(): {first:?}");}length: Some(2)
nickname still there: Some("bo")
after as_mut: Some("bobby")
as_deref: bobby and anonymous
get(1): Some(20)
get(1).copied(): Some(20)
get(7).copied(): None
first().cloned(): Some("ana")get is the checked way to read a position: it returns None past the end, as get(7) shows.
Taking a value out: take and replace
Ownership covers why you cannot move a value out of a borrowed place, and why Option::take is the fix for an Option field behind &mut self. take() leaves None behind. replace(v) puts a new Some(v) in and returns the old one:
struct Mailbox { letter: Option<String>,}
impl Mailbox { // take: hand the letter over and leave None behind. fn collect(&mut self) -> Option<String> { self.letter.take() }
// replace: put a new letter in and get the old one back. fn deliver(&mut self, text: &str) -> Option<String> { self.letter.replace(String::from(text)) }}
fn main() { let mut mailbox = Mailbox { letter: None }; println!("deliver 'hi': old = {:?}", mailbox.deliver("hi")); println!("deliver 'again': old = {:?}", mailbox.deliver("again")); println!("collect: {:?}", mailbox.collect()); println!("collect again: {:?}", mailbox.collect()); println!("now: {:?}", mailbox.letter);}deliver 'hi': old = None
deliver 'again': old = Some("hi")
collect: Some("again")
collect again: None
now: Noneunwrap and expect
unwrap() returns the value inside, and panics if there is none. expect("message") does the same with your message. A panic ends the program (or the thread it happens in) with an error and a line number.
fn find_age(id: u32) -> Option<u32> { if id == 1 { Some(31) } else { None }}
fn main() { println!("user 1: {}", find_age(1).unwrap()); println!("user 2: {}", find_age(2).unwrap());}user 1: 31 thread 'main' (1) panicked at src/main.rs:7:40: called `Option::unwrap()` on a `None` value note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
That is the old null crash again, so do not use it just to make an error go away. It is fine when None would be a bug in your own program:
| Fine | Not fine |
|---|---|
| Tests and examples, where a crash is the failure you want to see | Data from a user, a file or the network |
A value you have just proved is there, with expect saying why |
“It’s never missing in practice” |
| Quick scripts and prototypes you will tidy later | Code other people run, such as a server or a library |
fn main() { // The text is a literal in this file, and it has a colon. Checking it by hand // proves split_once cannot return None, so expect states why. let setting = "mode: fast"; let (key, value) = setting.split_once(": ").expect("the literal contains ': '"); println!("key: {key}, value: {value}");
// The list is built just above and is never empty. let scores = vec![72, 95, 88]; let first = scores.first().expect("scores has three items"); println!("first score: {first}");}key: mode, value: fast first score: 72
The message says why None is impossible, which is what the next reader needs when it happens. The std docs suggest wording like expect("slice should not be empty").
Option or Result?
Both say “this might not work”. Option says only that there is nothing. Result says why. Pick by what the caller can do with the answer.
| Use | When |
|---|---|
Option<T> |
Absence is normal and the reason is obvious: a lookup that found nothing, the first item of an empty list, an optional field |
Result<T, E> |
Something was attempted and failed, and the caller may want to know how: parsing, opening a file, a network call |
The same parse as each. .ok() turns a Result into an Option and drops the reason; ok_or goes the other way:
fn main() { // Result keeps the reason a parse failed. Option has no room for one. println!("parse '42': {:?}", "42".parse::<u32>()); println!("parse '4x2': {:?}", "4x2".parse::<u32>());
// .ok() turns a Result into an Option and drops the reason. println!("parse '42', then ok(): {:?}", "42".parse::<u32>().ok()); println!("parse '4x2', then ok(): {:?}", "4x2".parse::<u32>().ok());
// ok_or goes the other way: it gives a None a reason. println!("None.ok_or(...): {:?}", None::<u32>.ok_or("no value given"));}parse '42': Ok(42)
parse '4x2': Err(ParseIntError { kind: InvalidDigit })
parse '42', then ok(): Some(42)
parse '4x2', then ok(): None
None.ok_or(...): Err("no value given")The next lesson covers Result, and ? on it.
Going deeper
Option<&T> takes no extra space
Enums explains how Rust lays this out. In short: a reference in safe Rust is never null, so the all-zero address is free to mean None, and Option<&T> is the same size as &T. The std docs guarantee that for sized &T, &mut T, Box<T>, function pointers and the NonZero numbers. An ordinary number has no spare pattern, so Option<i32> needs a tag, and a raw pointer may legitimately be null, so Option<*const i32> needs one too:
use std::mem::size_of;use std::num::NonZeroU32;
fn main() { // A reference is never null, so None can use the all-zero address and no tag is needed. println!("size of &i32: {}", size_of::<&i32>()); println!("size of Option<&i32>: {}", size_of::<Option<&i32>>()); println!("size of Box<i32>: {}", size_of::<Box<i32>>()); println!( "size of Option<Box<i32>>: {}", size_of::<Option<Box<i32>>>() ); println!("size of NonZeroU32: {}", size_of::<NonZeroU32>()); println!( "size of Option<NonZeroU32>: {}", size_of::<Option<NonZeroU32>>() );
// Every bit pattern of an i32 is a number, so Option<i32> needs room for a tag. println!("size of i32: {}", size_of::<i32>()); println!("size of Option<i32>: {}", size_of::<Option<i32>>()); println!("size of *const i32: {}", size_of::<*const i32>()); println!( "size of Option<*const i32>: {}", size_of::<Option<*const i32>>() );}size of &i32: 8 size of Option<&i32>: 8 size of Box<i32>: 8 size of Option<Box<i32>>: 8 size of NonZeroU32: 4 size of Option<NonZeroU32>: 4 size of i32: 4 size of Option<i32>: 8 size of *const i32: 8 size of Option<*const i32>: 16
Option inside Option
A Some(None) is a real value: “there was an answer, and the answer is nothing”. A map (keys looked up to find values; Maps comes later) whose values are optional shows why it matters. One lookup tells “the key holds nothing” from “no such key”:
use std::collections::HashMap;
fn main() { // A map from item name to a count that may itself be unknown. let mut stock: HashMap<&str, Option<u32>> = HashMap::new(); stock.insert("apple", Some(3)); stock.insert("pear", None);
// One lookup keeps "the key holds None" apart from "no such key". println!("get apple: {:?}", stock.get("apple")); println!("get pear: {:?}", stock.get("pear")); println!("get plum: {:?}", stock.get("plum"));}get apple: Some(Some(3)) get pear: Some(None) get plum: None
How other languages handle missing values
| Language | A value that might be missing | Enforced? |
|---|---|---|
| Rust | Option<T> |
Yes: it is a different type from T |
| Kotlin | T? |
Yes, by the compiler, except for Java values (platform types) |
| TypeScript | T | undefined (or null) |
Yes with strictNullChecks (part of strict) |
| C# | T? |
Warnings for reference types (CS8602), which you can turn into errors. int? is stricter. |
| Java | Optional<T>, but any reference can be null |
No |
| Go | pointer, nil, or (value, ok) |
Partly: the pair must be used or discarded with _, but m[k] alone and nil pointers are unchecked |
| Python | Optional[T] or T | None |
Only if you run a checker such as mypy |
| C | NULL |
No |
| C++ | std::optional<T>, or a pointer |
Partly: *opt on an empty optional is undefined behaviour |
Practice
Try it
Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed. Every one checks the None path.
Chain a lookup
Given owner(pet) and city(person), which both return an Option, write pet_location: “rex lives in Lisbon” when both lookups succeed, and “rex: unknown” when either fails. One chain of and_then, map and unwrap_or_else does it, with no match. A pet whose owner has no city is the case to watch.
/// The owner of a pet, if we know one.fn owner(pet: &str) -> Option<&'static str> { match pet { "rex" => Some("ana"), "tom" => Some("bo"), "fido" => Some("cy"), _ => None, }}
/// The city a person lives in, if we know it.fn city(person: &str) -> Option<&'static str> { match person { "ana" => Some("Lisbon"), "bo" => Some("Oslo"), _ => None, }}
/// "rex lives in Lisbon" when the pet's owner and the owner's city are both known,/// and "rex: unknown" when either is missing.fn pet_location(pet: &str) -> String { todo!("owner(pet), then city(...), then format the answer, with a fallback for None")}
fn main() { assert_eq!(pet_location("rex"), "rex lives in Lisbon"); assert_eq!(pet_location("tom"), "tom lives in Oslo"); // fido has an owner, but the owner has no known city. assert_eq!(pet_location("fido"), "fido: unknown"); // ghost has no owner at all. assert_eq!(pet_location("ghost"), "ghost: unknown"); assert_eq!(pet_location(""), ": unknown");
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
/// The owner of a pet, if we know one.fn owner(pet: &str) -> Option<&'static str> { match pet { "rex" => Some("ana"), "tom" => Some("bo"), "fido" => Some("cy"), _ => None, }}
/// The city a person lives in, if we know it.fn city(person: &str) -> Option<&'static str> { match person { "ana" => Some("Lisbon"), "bo" => Some("Oslo"), _ => None, }}
/// "rex lives in Lisbon" when the pet's owner and the owner's city are both known,/// and "rex: unknown" when either is missing.fn pet_location(pet: &str) -> String { owner(pet) .and_then(city) .map(|city| format!("{pet} lives in {city}")) .unwrap_or_else(|| format!("{pet}: unknown"))}
fn main() { assert_eq!(pet_location("rex"), "rex lives in Lisbon"); assert_eq!(pet_location("tom"), "tom lives in Oslo"); // fido has an owner, but the owner has no known city. assert_eq!(pet_location("fido"), "fido: unknown"); // ghost has no owner at all. assert_eq!(pet_location("ghost"), "ghost: unknown"); assert_eq!(pet_location(""), ": unknown");
println!("all checks passed");}all checks passed
Parse a point with ?
Turn "3,4" into Some((3, 4)). Anything that is not two whole numbers around one comma gives None. Use ? on split_once(','), and on .parse().ok() for each side: parse returns a Result, .ok() turns it into an Option and drops the error (like v, _ := in Go, or try/except returning None in Python), and Rust works out the number type from the return type. Empty input and a missing side are in the checks.
/// Turn "3,4" into Some((3, 4)). Anything that is not two whole numbers/// separated by a comma gives None.fn parse_point(text: &str) -> Option<(i32, i32)> { todo!("split_once(',')? then parse each side: .parse().ok()?")}
fn main() { assert_eq!(parse_point("3,4"), Some((3, 4))); assert_eq!(parse_point("-3,40"), Some((-3, 40))); // No comma. assert_eq!(parse_point("34"), None); // Empty input. assert_eq!(parse_point(""), None); // A side that is not a number, or is missing. assert_eq!(parse_point("3,x"), None); assert_eq!(parse_point("x,4"), None); assert_eq!(parse_point("3,"), None); // Three parts: "4,5" is not a number. assert_eq!(parse_point("3,4,5"), None);
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
/// Turn "3,4" into Some((3, 4)). Anything that is not two whole numbers/// separated by a comma gives None.fn parse_point(text: &str) -> Option<(i32, i32)> { let (x, y) = text.split_once(',')?; let x = x.parse().ok()?; let y = y.parse().ok()?; Some((x, y))}
fn main() { assert_eq!(parse_point("3,4"), Some((3, 4))); assert_eq!(parse_point("-3,40"), Some((-3, 40))); // No comma. assert_eq!(parse_point("34"), None); // Empty input. assert_eq!(parse_point(""), None); // A side that is not a number, or is missing. assert_eq!(parse_point("3,x"), None); assert_eq!(parse_point("x,4"), None); assert_eq!(parse_point("3,"), None); // Three parts: "4,5" is not a number. assert_eq!(parse_point("3,4,5"), None);
println!("all checks passed");}all checks passed
Nickname or real name
Return the nickname if there is one that is not empty, otherwise the real name. The nickname arrives as &Option<String>, so you cannot move it: as_deref, filter and unwrap_or do it in one chain. An empty nickname is the case to watch.
/// The nickname if there is one that is not empty, otherwise the real name.fn display_name(nickname: &Option<String>, real: &str) -> String { todo!("as_deref turns &Option<String> into Option<&str>; then filter, then unwrap_or")}
fn main() { assert_eq!(display_name(&Some(String::from("bo")), "Robert"), "bo"); assert_eq!(display_name(&None, "Robert"), "Robert"); // An empty nickname counts as no nickname. assert_eq!(display_name(&Some(String::new()), "Robert"), "Robert"); // The input is only borrowed, so it is still usable afterwards. let kept = Some(String::from("bo")); assert_eq!(display_name(&kept, ""), "bo"); assert_eq!(kept, Some(String::from("bo")));
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
/// The nickname if there is one that is not empty, otherwise the real name.fn display_name(nickname: &Option<String>, real: &str) -> String { nickname .as_deref() .filter(|n| !n.is_empty()) .unwrap_or(real) .to_string()}
fn main() { assert_eq!(display_name(&Some(String::from("bo")), "Robert"), "bo"); assert_eq!(display_name(&None, "Robert"), "Robert"); // An empty nickname counts as no nickname. assert_eq!(display_name(&Some(String::new()), "Robert"), "Robert"); // The input is only borrowed, so it is still usable afterwards. let kept = Some(String::from("bo")); assert_eq!(display_name(&kept, ""), "bo"); assert_eq!(kept, Some(String::from("bo")));
println!("all checks passed");}all checks passed
Read more
Optionin the standard library docs: every method, each with a runnable example. Thestd::optionmodule page has the “The question mark operator” and “Representation” sections.- The Rust Book, chapter 6.1: Defining an Enum, where
Optionand the billion-dollar mistake come in, and 6.3: Concise Control Flow withif letandlet...else. - Rust by Example: Option and Combinators:
map. - Reading compiler errors: E0004, E0369, E0308, E0382, E0277 and E0609 from this page, with what each means.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.