Skip to content
Lesson 3.2Typed Memory, Ownership, and Object Layouts

Rust: Missing Values, Failures, and States

Follow Option and Result through closures, early returns, and pattern matching, then represent game states with enums.

Lesson 3.2
Read or listenPrint chapter or book
About 22 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 3.2Key ideas, reference and questions

A one-page summary of “Rust: Missing Values, Failures, and States” you can keep beside your notes. Plain Markdown.

Lesson 3.2 — Rust: Missing Values, Failures, and States: cheatsheet

  • Remember: A pattern identifies the value's shape. A guard tests a condition. The selected arm produces an answer; only one arm runs.
  • Also useful: Think of an assembly line with a stop button: each step passes the part along, and the first bad part halts the line and goes back to the caller.

Quick reference

  • Change an optional success: option.map(f) — Runs f only for Some.
  • Chain optional work: option.and_then(f) — f returns Option; no extra nesting.
  • Supply a missing-value reason: option.ok_or(error) — Produces Result.
  • Change a failure reason: result.map_err(f) — Runs f only for Err.
  • Chain fallible work: result.and_then(f) — f returns Result.
  • Leave on failure: result? — Continues with success or returns the error.
  • Store distinct states: enum with data — Each variant carries its own fields.
  • Bind or exit: let Pattern = value else { return ...; }; — Rejects the wrong shape early.
  • Ask about a pattern: matches!(value, pattern) — Produces bool.
  • Refine an arm: pattern if condition => ... — Checks the guard before selecting the arm.

Review

  • Q: A lookup may return no value. A calculation should run only when a value exists. What does mapping the optional value do? A: Transform the contained value, preserving absence.
  • Q: A successful lookup is followed by another lookup that can also be absent. Which composition avoids a nested optional value? A: Chain the fallible lookup and keep one optional layer.
Download cheatsheet (.md)

The Rust Primer introduced Some, None, Ok, and Err. Here we follow those shapes through several operations. An inventory slot may be missing, a purchase may fail, and a dash may be cooling down. Each case deserves a value that tells the caller what happened.

The examples use ordinary arrays and numbers in an invented game. The traces simulate those examples in the page; changing a control never reads or writes another program’s memory. Every trace starts with a worked run. Typing and recall practice are optional.

A checked subtraction can report a dash that cannot be made. A missing answer needs a different shape from a valid result of zero:

fn remaining_after_dash(stamina: u32, cost: u32) -> Option<u32> {
stamina.checked_sub(cost)
}

checked_sub gives Some(8) for (12, 4), because the subtraction fits in u32. It gives None for (2, 4), because an unsigned value cannot hold the negative answer. For (4, 4), the answer is Some(0), not None; zero is a valid remaining amount. This differs from dash, which returned false and left the runner’s stamina unchanged on a failed dash. The return type tells the caller which story it will receive. In Rust, the last expression without a semicolon is the returned value; here that expression is the checked_sub result.

Some zero is different from no answer. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let remaining = stamina.checked_sub(cost);
match remaining {
    Some(left) => println!("Left: {left}"),
    None => println!("Not enough stamina"),
}

The result is Some(0); the caller can use the stored number.

Final values: stamina = 4; cost = 4; remaining = Some(0); left = 0; output = Left: 0.

Read every step of the lesson’s example
  1. Step 1, line 1. Ask for 4 minus 4 without allowing an unsigned underflow.

    stamina = 4; cost = 4.

  2. Step 2, line 1. The answer fits, so checked_sub returns Some(0). Zero is a valid number.

    stamina = 4; cost = 4; remaining = Some(0).

  3. Step 3, line 2. match selects the arm for the Some shape.

    stamina = 4; cost = 4; remaining = Some(0).

  4. Step 4, line 3. The Some arm names the number left and prints 0.

    stamina = 4; cost = 4; remaining = Some(0); left = 0; output = Left: 0.

  5. Step 5, line 5. The result is Some(0); the caller can use the stored number.

    stamina = 4; cost = 4; remaining = Some(0); left = 0; output = Left: 0.

The default run returns Some(0). Lower stamina below the cost and the result becomes None; it never manufactures a negative unsigned value.

Another common Rust type, Result, represents either a success (Ok) or an error (Err) when the reason for failure matters. The official Option documentation has more examples when you need them.

An Option<u32> contains a number or contains no number. map applies a function to the number inside Some, then puts the answer back in Some. For None, it returns None without calling the function.

let price = Some(8_u32);
let doubled = price.map(|n| n * 2); // Some(16)

|n| n * 2 is a closure: a small function written where it is needed. The bars name its input; the expression computes its output. A closure may also borrow values from the surrounding scope, such as budget below. Closures are functions with capture; they do not run merely because you write them down.

Chain another optional operation with and_then

Section titled “Chain another optional operation with and_then”

The next calculation can itself have no answer: an unsigned subtraction cannot represent a negative amount. and_then calls a function returning another Option, and keeps a single layer of Some or None.

fn quoted_balance(slot: usize, budget: u32) -> Result<u32, &'static str> {
let costs = [4_u32, 8, 12];
let price = costs.get(slot).copied();
let doubled = price.map(|n| n * 2);
let left = doubled.and_then(|n| budget.checked_sub(n));
left.ok_or("not affordable")
}

With slot 1 and budget 20, the sequence is Some(8) → Some(16) → Some(4) → Ok(4). With slot 4, get returns None; both closures are skipped. ok_or turns a missing answer into an error with a reason. This example deliberately gives the same reason for a missing item and a high price. Use distinct error variants when callers need to distinguish them.

Three slots through quoted_balanceBudget 20 and costs = [4, 8, 12]. Each step works on the result of the step before; once a slot’s value is None, the later closures are skipped.
  1. 1 costs.get(slot).copied()

    Some(8)slot 1
    Some(12)slot 2
    Noneslot 4: no item

    Slot 4 is past the end of the three prices, so get returns None.

  2. 2 map(|n| n * 2)

    Some(16)slot 1
    Some(24)slot 2
    Noneslot 4: skipped

    The closure runs only for Some: 8 × 2 = 16 and 12 × 2 = 24.

  3. 3 and_then(|n| budget.checked_sub(n))

    Some(4)slot 1
    Noneslot 2: 20 − 24 fails
    Noneslot 4: skipped

    20 − 16 = 4 fits; 20 − 24 would be −4, so checked_sub gives None.

  4. 4 ok_or("not affordable")

    Ok(4)slot 1
    Errslot 2: not affordable
    Errslot 4: not affordable

    None becomes Err with the one chosen reason, whatever caused it.

Budget 20 and costs = [4, 8, 12]. Each step works on the result of the step before; once a slot’s value is None, the later closures are skipped.

Transform a value, then ask a fallible question. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let costs = [4_u32, 8, 12];
let price = costs.get(slot).copied();
let doubled = price.map(|n| n * 2);
let left = doubled.and_then(|n| budget.checked_sub(n));
let result = left.ok_or("not affordable");

ok_or keeps the successful amount, including zero.

Final values: slot = 1; budget = 20; costs = [4, 8, 12]; price = Some(8); doubled = Some(16); left = Some(4); result = Ok(4).

Read every step of the lesson’s example
  1. Step 1, line 1. The array owns three prices. A slot beyond its length has no value.

    slot = 1; budget = 20; costs = [4, 8, 12].

  2. Step 2, line 2. get lends the price; copied copies the u32 into Some(8).

    slot = 1; budget = 20; costs = [4, 8, 12]; price = Some(8).

  3. Step 3, line 3. map calls the closure once: 8 times 2 is 16.

    slot = 1; budget = 20; costs = [4, 8, 12]; price = Some(8); doubled = Some(16).

  4. Step 4, line 4. checked_sub returns Some(4); and_then avoids Some(Some(...)).

    slot = 1; budget = 20; costs = [4, 8, 12]; price = Some(8); doubled = Some(16); left = Some(4).

  5. Step 5, line 5. ok_or keeps the successful amount, including zero.

    slot = 1; budget = 20; costs = [4, 8, 12]; price = Some(8); doubled = Some(16); left = Some(4); result = Ok(4).

Option answers whether a value exists; Result can explain a failure:

Question Rust shape Example
Might there be no value? Option<T>: Some or None No item at index 5.
Might an operation fail with a reason? Result<T, E>: Ok or Err Text is not a whole number.

T and E name the success-value and error types. Lesson 3.1 applies this distinction to a memory reader and explains where to handle errors.

Result::map changes an Ok value and leaves Err alone. map_err changes an error and leaves Ok alone. and_then chains another operation that can return an error. The success path keeps flowing; the first error passes through without running later success closures.

fn doubled_score(reading: Result<u8, &'static str>) -> Result<u8, &'static str> {
reading
.map(|n| u16::from(n) * 2)
.and_then(|n| u8::try_from(n).map_err(|_| "score too large"))
}

Widening to u16 before multiplication gives room for double any u8. try_from checks whether that answer fits back into u8. Converting an error with map_err is an explicit choice: here the exact numeric conversion error is replaced with a short domain description.

Result map changes success; and_then can fail again. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let reading: Result<u8, &str> = input;
let score = reading.map(|n| u16::from(n) * 2);
let small = score.and_then(|n| u8::try_from(n)
    .map_err(|_| "score too large"));

The next operation succeeds, so and_then keeps one Result layer.

Final values: reading = Ok(100); score = Ok(200); small = Ok(200).

Read every step of the lesson’s example
  1. Step 1, line 1. A successful reading carries a u8.

    reading = Ok(100).

  2. Step 2, line 2. map transforms 100 into 200 inside Ok. Widening first prevents u8 multiplication overflow.

    reading = Ok(100); score = Ok(200).

  3. Step 3, line 3. The next operation succeeds, so and_then keeps one Result layer.

    reading = Ok(100); score = Ok(200); small = Ok(200).

? takes the value out of Ok so the next line can use it. On Err, it returns from the enclosing function immediately, converting the error to the function’s error type through From when necessary. It does not retry, recover, or turn an error into a default value.

fn buy(text: &str, gold: u32) -> Result<u32, &'static str> {
let cost = text.parse::<u32>().map_err(|_| "bad cost")?;
let left = gold.checked_sub(cost).ok_or("not enough gold")?;
Ok(left)
}

The default trace buys a 4-gold item with 12 gold. Invalid text stops before subtraction; an excessive cost stops before Ok(left). Returning zero gold after an exact-price purchase is still success.

A reason travels through Result and question mark. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

fn buy(text: &str, gold: u32) -> Result<u32, &'static str> {
    let cost = text.parse::<u32>().map_err(|_| "bad cost")?;
    let left = gold.checked_sub(cost).ok_or("not enough gold")?;
    Ok(left)
}

The caller receives success and can choose what to do with the remaining amount.

Final values: text = 4; gold = 12; cost = 4; left = 8; result = Ok(8).

Read every step of the lesson’s example
  1. Step 1, line 1. The function promises either remaining gold or an error reason.

    text = 4; gold = 12.

  2. Step 2, line 2. Parsing gives Ok(4). map_err skips its closure; ? supplies 4 to cost.

    text = 4; gold = 12; cost = 4.

  3. Step 3, line 3. Subtraction fits. ok_or makes Ok and ? takes out its value.

    text = 4; gold = 12; cost = 4; left = 8.

  4. Step 4, line 4. The caller receives success and can choose what to do with the remaining amount.

    text = 4; gold = 12; cost = 4; left = 8; result = Ok(8).

The previous purchase used short domain reasons. This parser instead propagates the standard library’s parse error unchanged, then shows both inputs through a complete caller:

fn read_dash_cost(text: &str) -> Result<u32, std::num::ParseIntError> {
let cost = text.parse::<u32>()?;
Ok(cost)
}
fn main() {
for text in ["4", "tired"] {
match read_dash_cost(text) {
Ok(cost) => println!("Cost: {cost}"),
Err(error) => println!("Invalid cost: {error}"),
}
}
}

For "4", parse produces Ok(4), ? gives 4 to cost, and the function returns Ok(4). For "tired", parse produces Err, so the function stops before Ok(cost) and passes that error to its caller. The caller’s match still decides what to do with it. Read ? as “continue with the value, or return the error now.”

The angle brackets in Result<u32, ParseIntError> supply concrete types to a reusable type: success holds a u32, error holds a parse error. Option<u32>, Vec<u32>, and HashMap<&str, u32> use the same generic pattern with different values inside.

Separate ready: bool and ticks: u32 fields permit contradictory combinations such as “ready with five cooldown ticks.” An enum with data puts the alternatives into one value:

enum Dash {
Ready,
Cooling { ticks: u32 },
}
fn tick(state: Dash) -> Dash {
match state {
Dash::Cooling { ticks } if ticks > 1 => Dash::Cooling { ticks: ticks - 1 },
Dash::Cooling { .. } => Dash::Ready,
Dash::Ready => Dash::Ready,
}
}

Cooling carries a count. Ready carries none. tick takes ownership of the old state and returns the next one. The guard if ticks > 1 adds a condition to the first matching pattern; the following arm handles the remaining cooldown counts. .. means those fields are not needed here.

Only the current state carries its data. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

enum Dash { Ready, Cooling { ticks: u32 } }
let state = Dash::Cooling { ticks };
let next = match state {
    Dash::Cooling { ticks } if ticks > 1 => Dash::Cooling { ticks: ticks - 1 },
    Dash::Cooling { .. } => Dash::Ready,
    Dash::Ready => Dash::Ready,
};

Exactly one arm produced the next state; every variant has an arm.

Final values: state = Cooling { ticks: 3 }; ticks = 3; next = Cooling { ticks: 2 }.

Read every step of the lesson’s example
  1. Step 1, line 2. Cooling contains the count needed by that state. There is no separate ready flag to contradict it.

    state = Cooling { ticks: 3 }.

  2. Step 2, line 3. match checks the variant, then its optional guard.

    state = Cooling { ticks: 3 }; ticks = 3.

  3. Step 3, line 4. The guard is true: one tick elapses and the next state carries the smaller count.

    state = Cooling { ticks: 3 }; ticks = 3; next = Cooling { ticks: 2 }.

  4. Step 4, line 7. Exactly one arm produced the next state; every variant has an arm.

    state = Cooling { ticks: 3 }; ticks = 3; next = Cooling { ticks: 2 }.

Use patterns for shape and guards for conditions

Section titled “Use patterns for shape and guards for conditions”

let else names values from the expected shape and exits otherwise. Its else branch must diverge: for example, return from the function. matches! asks whether a value matches a pattern and produces a bool. A match guard checks an additional condition after a pattern matches.

fn next_action(player_health: Option<u32>) -> &'static str {
let Some(health) = player_health else { return "no player"; };
let alive = matches!(health, 1..=100);
match health {
n if n < 25 && alive => "heal",
0 => "respawn",
_ => "wait",
}
}

1..=100 includes both endpoints. A player at zero health does not take the healing arm, because alive is false. The final _ catches other numeric values. For an enum representing distinct application states, prefer named arms so adding a new variant requires a deliberate decision.

Name the successful shape, then test its meaning. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let Some(health) = player_health else { return "no player"; };
let alive = matches!(health, 1..=100);
match health {
    n if n < 25 && alive => "heal",
    0 => "respawn",
    _ => "wait",
}

The selected arm returns heal.

Final values: player_health = Some(20); result = heal; health = 20; alive = true.

Read every step of the lesson’s example
  1. Step 1, line 1. let else names the health inside Some. Its else branch is skipped.

    player_health = Some(20); result = not chosen.

  2. Step 2, line 2. matches! produces a bool; it does not bind a new variable outside the macro.

    player_health = Some(20); result = not chosen; health = 20; alive = true.

  3. Step 3, line 3. Arms are tried in order, and a guard may reject an otherwise matching pattern.

    player_health = Some(20); result = not chosen; health = 20; alive = true.

  4. Step 4, line 4. The selected arm returns heal.

    player_health = Some(20); result = heal; health = 20; alive = true.

Need Expression What it does
Change an optional success option.map(f) Runs f only for Some.
Chain optional work option.and_then(f) f returns Option; no extra nesting.
Supply a missing-value reason option.ok_or(error) Produces Result.
Change a failure reason result.map_err(f) Runs f only for Err.
Chain fallible work result.and_then(f) f returns Result.
Leave on failure result? Continues with success or returns the error.
Store distinct states enum with data Each variant carries its own fields.
Bind or exit let Pattern = value else { return ...; }; Rejects the wrong shape early.
Ask about a pattern matches!(value, pattern) Produces bool.
Refine an arm pattern if condition => ... Checks the guard before selecting the arm.

Use the Option and Result references to check exact signatures. Next, collections and bytes connect these outcome shapes to bounded buffers and repeated work.

Knowledge check

Reason about the concept

A lookup may return no value. A calculation should run only when a value exists. What does mapping the optional value do?

Finished Lesson 3.2?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 3: 11 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.