Rust: Missing Values, Failures, and States
Follow Option and Result through closures, early returns, and pattern matching, then represent game states with enums.
Cheatsheet for Lesson 3.2
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)— Runsfonly forSome. - Chain optional work:
option.and_then(f)—freturnsOption; no extra nesting. - Supply a missing-value reason:
option.ok_or(error)— ProducesResult. - Change a failure reason:
result.map_err(f)— Runsfonly forErr. - Chain fallible work:
result.and_then(f)—freturnsResult. - Leave on failure:
result?— Continues with success or returns the error. - Store distinct states:
enumwith 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)— Producesbool. - 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.
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.
When a result might be missing
Section titled “When a result might be missing”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
Step 1, line 1. Ask for 4 minus 4 without allowing an unsigned underflow.
stamina = 4; cost = 4.
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).
Step 3, line 2. match selects the arm for the Some shape.
stamina = 4; cost = 4; remaining = Some(0).
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.
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.
Transform a present value with map
Section titled “Transform a present value with map”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.
1 costs.get(slot).copied()
Some(8)slot 1Some(12)slot 2Noneslot 4: no itemSlot 4 is past the end of the three prices, so get returns None.
2 map(|n| n * 2)
Some(16)slot 1Some(24)slot 2Noneslot 4: skippedThe closure runs only for Some: 8 × 2 = 16 and 12 × 2 = 24.
3 and_then(|n| budget.checked_sub(n))
Some(4)slot 1Noneslot 2: 20 − 24 failsNoneslot 4: skipped20 − 16 = 4 fits; 20 − 24 would be −4, so checked_sub gives None.
4 ok_or("not affordable")
Ok(4)slot 1Errslot 2: not affordableErrslot 4: not affordableNone 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
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].
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).
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).
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).
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).
Carry a failure reason through Result
Section titled “Carry a failure reason through Result”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
Step 1, line 1. A successful reading carries a u8.
reading = Ok(100).
Step 2, line 2. map transforms 100 into 200 inside Ok. Widening first prevents u8 multiplication overflow.
reading = Ok(100); score = Ok(200).
Step 3, line 3. The next operation succeeds, so and_then keeps one Result layer.
reading = Ok(100); score = Ok(200); small = Ok(200).
Let ? return the first error
Section titled “Let ? return the first error”? 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
Step 1, line 1. The function promises either remaining gold or an error reason.
text = 4; gold = 12.
Step 2, line 2. Parsing gives Ok(4). map_err skips its closure; ? supplies 4 to cost.
text = 4; gold = 12; cost = 4.
Step 3, line 3. Subtraction fits. ok_or makes Ok and ? takes out its value.
text = 4; gold = 12; cost = 4; left = 8.
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).
Stop a sequence early with ?
Section titled “Stop a sequence early with ?”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.
Give each state only the data it needs
Section titled “Give each state only the data it needs”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
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 }.
Step 2, line 3. match checks the variant, then its optional guard.
state = Cooling { ticks: 3 }; ticks = 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 }.
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
Step 1, line 1. let else names the health inside Some. Its else branch is skipped.
player_health = Some(20); result = not chosen.
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.
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.
Step 4, line 4. The selected arm returns heal.
player_health = Some(20); result = heal; health = 20; alive = true.
Cheatsheet: outcomes and states
Section titled “Cheatsheet: outcomes and states”| 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.
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?
Mapping changes a present value; absence skips the transformation and remains absent.
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.
Plain Markdown, saved in this browser.
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.