Skip to content

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 a T, 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 a T (success) or an E (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.

🦀 main.rs

Option has a bunch of helper methods so you don’t have to match every time. A few of the everyday ones:

🦀 main.rs

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.

🦀 main.rs

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’s Err(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:

🦀 main.rs

A few neat things in that snippet:

  • main itself returns a Result. 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 the Error trait.” A nice catch-all for top-level handlers.
  • The ? operator also does an automatic conversion of error types (via the From trait), 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:

🦀 main.rs

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

🦀 main.rs

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 of match arms.
  • 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.