129. Option and Result
Rust has no null and no exceptions. A missing value and a failed step are ordinary enums, the compiler makes you look inside them, and one character, ?, passes failures up the call stack.
- Return Option when a value may be absent, and get the value out with match, if let or unwrap_or.
- Return Result when a step can fail, and say what Ok and Err carry.
- Use the ? operator to pass an error or a None up to the caller, and say where it is allowed.
- Compare Drip's TypeScript VM, which throws runtime errors, with drip-rs, which returns them.
Where did the value go?
You have written this TypeScript many times:
const xs = [4, 5, 6];
const i = xs.indexOf(9); // -1
const p = ps.find((p) => p.x > 100);
console.log(p.x); // crash if none matched
Two ways of saying “nothing here”: -1 and undefined. With
strictNullChecks, TypeScript flags the second, because find returns
P | undefined, but -1 is a perfectly good number. And failures are
another story. When the Drip VM from Part IV meets 1 - true, it
throws, and nothing in the type of the function that threw warns
you.
Rust takes both ideas away. There is no null or undefined, and there
are no exceptions. Absence and failure are ordinary values, of two enums
you already know how to read.
Think of a parcel locker. You get a slip back, and the slip says either “box 7” or “nothing for you today”. You can’t open box 7 without first reading the slip, and you can’t misread “nothing” as a box number. Rust hands you the slip, not the parcel.
Option: maybe a value
Option is an enum from the standard library, just like the ones in
chapter 128. Simplified, its definition is:
enum Option<T> {
None,
Some(T),
}
The <T> says “some type, to be filled in”: Option<usize> is maybe a
usize, Option<&str> maybe a string slice. (Chapter 132 is all about
those angle brackets; for now read them as TypeScript’s Array<T>.)
Option, Some and None are so common that you can use them without
importing anything.
Here is indexOf done honestly:
fn find(xs: &[i32], t: i32)
-> Option<usize> {
for i in 0..xs.len() {
if xs[i] == t {
return Some(i);
}
}
None
}
(for i in 0..xs.len() counts i from 0 up to the length minus one,
like TypeScript’s counting loop; chapter 134 explains Rust’s for fully.)
The return type now says “maybe an index”, and the caller can’t pretend
otherwise. Try to use the answer as a number:
let i = find(&[4, 5], 5);
println!("{}", i + 1);
Rust 1.97 on this machine refuses (trimmed):
error[E0369]: cannot add `{integer}`
to `Option<usize>`
12 | println!("{}", i + 1);
| - ^ - {integer}
| Option<usize>
An Option<usize> is not a usize, so you have to open the slip.
Option<T> is an enum with two variants: Some(value), holding a
T, and None, holding nothing. A function that might have no answer
returns Option<T>, and the type system stops the caller from using the
T until it has checked which variant it got.
Opening the slip
Option is an enum, so match works, and it must cover both Some(i)
and None. When you only care about one arm, if let is a one-arm match:
if let Some(i) = find(&xs, 5) { ... } runs the block only for Some,
with i bound. Two methods cover common cases. unwrap_or(d) gives the
value or a default d. unwrap() gives the value or panics: it stops
the program with a message. Use it only where None would be a bug, as
in a test.
You have met this idea before: Option<T> is a discriminated union, like
the ones in chapter 14, and match is narrowing that the compiler checks
for exhaustiveness.
fn find(xs: &[i32], t: i32)
-> Option<usize> {
for i in 0..xs.len() {
if xs[i] == t {
return Some(i);
}
}
None
}
fn main() {
let xs = [4, 5, 6];
match find(&xs, 5) {
Some(i) => println!("at {}", i),
None => println!("absent"),
}
if let Some(i) = find(&xs, 6) {
println!("6 is at {}", i);
}
let i = find(&xs, 9).unwrap_or(99);
println!("{}", i);
println!("{:?}", find(&xs, 4));
// Try: find(&xs, 9).unwrap()
}Result: it worked, or here is why not
Option says whether there is a value. When something fails you
usually want to know why. That is Result:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok carries the answer, of type T. Err carries the error, of type
E, which can be any type you like: a String message, or an enum of
error kinds (chapter 144). Here is the operand check from Drip’s VM,
cut down to two value kinds:
enum Value {
Num(f64),
Bool(bool),
}
fn as_num(v: &Value)
-> Result<f64, String> {
match v {
Value::Num(n) => Ok(*n),
_ => Err(String::from(
"Operands must be numbers.")),
}
}
The message is the real one: drip-rs and the TypeScript VM both print
Operands must be numbers. for 1 - true.
Result<T, E> is an enum with two variants: Ok(value) for
success, holding a T, and Err(error), holding an E that
describes the failure. A function that can fail returns Result, so its
failure is part of its type, and callers must handle it before they get
the T.
Passing it up with ?
Subtraction needs two numbers. Written with match, it would check each
operand and return early on Err, twice. That pattern is so common that
Rust gives it one character:
fn sub(a: &Value, b: &Value)
-> Result<f64, String> {
let x = as_num(a)?;
let y = as_num(b)?;
Ok(x - y)
}
as_num(a)? means: if this is Ok(n), the expression is n; if it is
Err(e), return Err(e) from sub right now. Step through a failing
call:
fn sub(a: &Value, b: &Value)-> Result<f64, String> {let x = as_num(a)?;let y = as_num(b)?;Ok(x - y)}// main: sub(&one, &t)
The ? operator, written after a Result, unwraps an Ok and
otherwise returns the Err from the enclosing function at once. On the
way it converts the error into the function’s error type with the From
trait[1], which matters once errors have several
types. After an Option, it unwraps Some and returns None. It works
only in a function whose return type can hold that early return.
That last rule is checked. Put ? in a function that returns i32 and
the compiler says (trimmed):
error[E0277]: the `?` operator can
only be used in a function that
returns `Result` or `Option` ...
enum Value {
Num(f64),
Bool(bool),
}
fn as_num(v: &Value)
-> Result<f64, String> {
match v {
Value::Num(n) => Ok(*n),
_ => Err(String::from(
"Operands must be numbers."
)),
}
}
fn sub(a: &Value, b: &Value)
-> Result<f64, String> {
let x = as_num(a)?;
let y = as_num(b)?;
Ok(x - y)
}
fn main() {
let one = Value::Num(1.0);
let t = Value::Bool(true);
println!("{:?}", sub(&one, &one));
println!("{:?}", sub(&one, &t));
match sub(&t, &one) {
Ok(n) => println!("{}", n),
Err(e) => println!("{}", e),
}
}Compare the two Drip VMs. The TypeScript one’s runtimeError records the
message and stack trace, then does throw new VmRuntimeError(message),
and interpret catches it. drip-rs (in book/companion/rust/src/vm.rs)
does the same bookkeeping, then returns a value. Its run loop has the
type Result<(), RuntimeError>, the arithmetic opcodes are written
let (a, b) = self.pop_numbers()?;, and returning from a call uses ?
on an Option: let caller = self.frames.last()?;. Each ? is one
early-return branch you can see in the source. And Option often costs
no space: the standard library guarantees that Option<&T> is the same
size as &T, because a reference is never null, so None can use the
null bit pattern[2]. On this machine
Option<&i32> is 8 bytes, like &i32, while Option<i32> takes 8
bytes to i32’s 4, since every i32 bit pattern is already a number.
Your turn
A toy VM stack is a slice of Values, with the top at the end. Write add_top(st), which returns the sum of the top two values. If there are fewer than two values, return Err("Stack underflow."). If either is not a number, return the error from as_num. Use ? for that.
enum Value {
Num(f64),
Bool(bool),
}
fn as_num(v: &Value)
-> Result<f64, String> {
match v {
Value::Num(n) => Ok(*n),
_ => Err(String::from(
"Operands must be numbers."
)),
}
}
fn add_top(st: &[Value])
-> Result<f64, String> {
todo!()
}Write top_two(st), which returns Some((below, top)) for the last two numbers of the slice, or None if there are fewer than two. Use st.len().checked_sub(2), which gives None instead of underflowing, together with ?.
fn top_two(st: &[f64])
-> Option<(f64, f64)> {
todo!()
}What you learned
Option<T>isSome(value)orNone. It replacesnull,undefinedand magic values like-1.- Get the value out with
match,if let,unwrap_or, orunwrap(which panics onNone). Result<T, E>isOk(value)orErr(error). Failure is in the function’s type, instead of thrown past it.?unwrapsOkorSome, and otherwise returns early with theErr(converted withFrom) or theNone. It only works in functions that return a matching type.- drip-rs returns its runtime errors where the TypeScript VM throws them.
Check your understanding
0/5- 1.
A Rust function might have no answer. What should it return?
- 2.
What happens at
let y = as_num(b)?;whenas_num(b)returnsErr(e)? - 3.
You write
?insidefn half(n: i32) -> i32. What happens? - 4.
Which TypeScript idea is closest to
Option<T>? - 5.
How does drip-rs report
1 - truecompared with the TypeScript VM?
References
- [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
- [2] The Rust Project Developers. Module core::option (Rust standard library documentation). Rust standard library API documentation; source in library/core/src/option.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/option/ (checked 2026-10-31)