Skip to content
Learn Rust
Theme

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

Rustsrc/main.rsRun ↗
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");
}
}
$ cargo run
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")
}
}
$ go run main.go
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")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
found ana
no user
from dataclasses import dataclass
@dataclass
class 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")
$ python3 main.py
found ana
no user
interface User {
name: string;
}
// with strictNullChecks, `undefined` must be part of the type to be returned
function 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 user
const found = findUser(2)?.name;
console.log(found ? `found ${found}` : "no user");
$ tsc --strict --module nodenext --types node main.ts && node main.js
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"));
}
}
$ javac Main.java && java Main
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 step
if (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);
$ dotnet run
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main
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";
}
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
found ana
no user

What's different

  • Option<User> is either Some(user) or None. 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.name is a closure, a small function written inline; Closures come later), and opt? 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 Result instead (next lesson). Option says "nothing"; Result says why.
Gotcha

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.
Rustsrc/main.rs✗ won't compile
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)
}
$ go run main.go! crashes at runtime
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 too

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
@dataclass
class 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)
$ python3 main.py! crashes at runtime
Traceback (most recent call last):
  File "main.py", line 14, in <module>
    print(u.name)
          ^^^^^^
AttributeError: 'NoneType' object has no attribute 'name'
TypeScript✗ refuses too

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());
}
}
$ javac Main.java && java Main! crashes at runtime
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);
$ dotnet run! crashes at runtime
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;
}
$ gcc -fsanitize=address -g main.c -o main && ./main! crashes at runtime
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==ABORTING
C++✗ refuses too

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.

#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.
$ cargo buildWhat is E0609?
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);
   |                      +++++++++
What happens in Go

Go compiles this, then crashes with a nil pointer panic when it reads u.Name.

What happens in Kotlin

Kotlin refuses this too: findUser returns User?, so u.name needs u?.name or a null check first.

What happens in Python

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.

What happens in TypeScript

TypeScript refuses this too with strict on: u is possibly undefined, so you must check it before reading u.name.

What happens in Java

It compiles, then throws a NullPointerException because findUser(2) returned null. Nothing made you check first.

What happens in C#

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.

What happens in C

C compiles this without a word. find_user(2) returns NULL, and AddressSanitizer reports a crash reading address 0.

What happens in C++

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.

What Rust sees

u is an Option<User>, not a User. An Option has no name field. You have to open it first.

Why Rust says no

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.

Rust, fixedsrc/main.rsRun ↗
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"),
}
}
$ cargo run
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.)

Rustsrc/main.rsRun ↗
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",
email: Some("ana@example.com"),
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"),
}
}
}
$ cargo run
id 1: ana, email ana@example.com, age Some(31)
id 2: bo, no email, age None
id 3: no such user

Leave out None and the program does not compile:

Rustsrc/main.rs✗ won't 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}"),
}
}
$ cargo buildWhat is E0004?
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 for Some. An else is optional.
  • let Some(x) = opt else { … }; unpacks the value, or runs the else block, which must leave (return, break, continue or a panic). The code after it keeps its normal indentation.
Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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());
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let age: Option<u32> = Some(30);
let next_year = age + 1;
println!("next year: {next_year}");
}
$ cargo buildWhat is E0369?
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:

Rustsrc/main.rs✗ won't compile
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}");
}
$ cargo buildWhat is E0308?
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.

Rustsrc/main.rsRun ↗
struct User {
name: &'static str,
email: Option<&'static str>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User {
name: "ana",
email: Some("ana@example.com"),
}),
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:?}");
}
}
$ cargo run
name length, user 1: Some(3)
name length, user 3: None
map to email, user 1: Some(Some("ana@example.com"))
map to email, user 2: Some(None)
and_then email, user 1: Some("ana@example.com")
and_then email, user 2: None
domain, user 1: Some("example.com")
domain, user 2: None
domain, user 3: None

Read 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.
Rustsrc/main.rsRun ↗
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());
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
struct User {
email: Option<&'static str>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User {
email: Some("ana@example.com"),
}),
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));
}
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
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}");
}
$ cargo buildWhat is E0277?
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:

Rustsrc/main.rsRun ↗
struct User {
email: Option<&'static str>,
age: Option<u32>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User {
email: Some("ana@example.com"),
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));
}
}
$ cargo run
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, "ana@example.com"))
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:

Rustsrc/main.rsRun ↗
struct User {
name: &'static str,
email: Option<&'static str>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User {
name: "ana",
email: Some("ana@example.com"),
}),
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 fallback = Some("noreply@example.com");
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: {:?}",
own(2).or_else(|| Some("noreply@example.com"))
);
}
$ cargo run
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("ana@example.com")
or, user 2: Some("noreply@example.com")
or_else, user 2: Some("noreply@example.com")

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:

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let nickname: Option<String> = Some(String::from("bo"));
let length = nickname.map(|n| n.len());
println!("length: {length:?}");
println!("nickname: {nickname:?}");
}
$ cargo buildWhat is E0382?
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
Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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);
}
$ cargo run
deliver 'hi': old = None
deliver 'again': old = Some("hi")
collect: Some("again")
collect again: None
now: None

unwrap 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.

Rustsrc/main.rs! panics at runtimeRun ↗
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());
}
$ cargo run
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
Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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"));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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>>()
);
}
$ cargo run
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”:

Rustsrc/main.rsRun ↗
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"));
}
$ cargo run
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.

Starterexercises/lookup-chain.rs
/// 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
One solutionexercises/lookup-chain.solution.rsRun ↗
/// 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");
}
$ cargo run
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.

Starterexercises/parse-point.rs
/// 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
One solutionexercises/parse-point.solution.rsRun ↗
/// 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");
}
$ cargo run
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.

Starterexercises/display-name.rs
/// 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
One solutionexercises/display-name.solution.rsRun ↗
/// 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");
}
$ cargo run
all checks passed

Read more