Error Handling
Rust does not throw
Most languages have exceptions. You write code that does the happy path, and
somewhere, magically, if anything goes wrong, control flow yeets itself out
of your function and lands… somewhere else. Maybe in a catch block three
calls up. Maybe in the user’s face.
Rust does not do that. Rust says: if your function can fail, its return type should say so. No throwing. No surprise control flow. Every failure is a value, and the compiler will make you decide what to do with it.
There are two canonical types for “this might fail,” and they’re both enums:
Option<T>— “maybe aT, maybe nothing.” Used when absence is a normal case, not an error. (pop()on an empty stack, or “find the user with this ID and return nothing if it doesn’t exist.”)Result<T, E>— “either aT(success) or anE(an error).” Used when failure is an unusual but expected possibility — file not found, parse error, network timeout.
Option — the “maybe” type
enum Option<T> { Some(T), None,}That’s the whole definition. Two variants. The standard library uses it
everywhere there’d otherwise be a null.
Option has a bunch of helper methods so you don’t have to match every
time. A few of the everyday ones:
Result — for things that go wrong on purpose
enum Result<T, E> { Ok(T), Err(E),}Two variants again: Ok holds the success value, Err holds the error.
By convention, anything that can fail returns a Result.
The ? operator — the killer feature
If error handling stopped at “use a match for every possible failure,” Rust
code would be a sea of indentation. Instead, Rust has the ? operator,
which is a tiny piece of syntactic magic that means:
If this is
Ok(value), give me the value. If it’sErr(e), return early from the enclosing function with that error.
So this:
fn read_number(s: &str) -> Result<i32, std::num::ParseIntError> { let n = match s.parse::<i32>() { Ok(v) => v, Err(e) => return Err(e), }; Ok(n + 1)}…becomes this:
A few neat things in that snippet:
mainitself returns aResult. That’s allowed since Rust 2018 — any unhandled error bubbles all the way up and becomes the program’s exit status.Box<dyn std::error::Error>means “any kind of error that implements theErrortrait.” A nice catch-all for top-level handlers.- The
?operator also does an automatic conversion of error types (via theFromtrait), so you can mix functions returning different error types in the same call chain. Magic, but legible magic.
Chaining failure-tolerant operations
Because Option and Result are real types with methods on them, you can
chain transformations cleanly:
.map, .and_then, .or_else, .unwrap_or_else — all of these let you
build little pipelines that gracefully short-circuit when something goes
wrong.
Panics — for things that should never happen
When the world is so wrong that there’s no sensible value to return at all
(an array index out of bounds, a logic bug, a state that should be impossible),
Rust offers panic!. It unwinds the stack and crashes the thread.
The rule of thumb: panics are for bugs, Results are for expected failures.
A file-not-found is a Result. An invariant violation in the middle of a
data structure is a panic!.
What you’ve got
You can now:
- Tell the difference between a thing that’s maybe missing (
Option) and a thing that’s expected to fail sometimes (Result). - Use
?to propagate errors without a forest ofmatcharms. - Use
match,if let,let else, and the iterator-style methods to handle failure inline. - Distinguish “shouldn’t happen, crash hard” from “might happen, return Err” in your own APIs.
That’s enough Rust to write real programs. The remaining chapters of this book — iterators, collections, lifetimes, smart pointers, concurrency, macros — are mostly about getting more elegant at what you can already do.
But that’s a story for another evening. Go take a break. Look at a tree. The borrow checker isn’t going anywhere.