← all lessons
Errors & Options · #10 of 13

Things Going Wrong — Option, Result, and ?

How Rust signals failure without exceptions

In most languages, a function that can fail looks exactly like one that can’t. The failure is invisible until, one day, at runtime, it isn’t.

Rust made a different bet: failure is a value, written into the type, impossible to ignore. There are no exceptions here — and that absence is a feature.

Every real program has to deal with things going wrong: a file that doesn’t exist, a network request that times out, a string that won’t parse into a number, a value that should be there but isn’t. The question every language has to answer is how a function tells its caller “that didn’t work.”

Most languages answer with exceptions — a hidden side-channel that lets an error fly up through the call stack until something catches it. Rust answers differently, and the difference shapes how you write everything.

A few words you’ll need

Errors are ordinary values

Here is the whole philosophy in one sentence: in Rust, a recoverable error is just a value you return, like any other. There is no second control-flow mechanism layered on top of the language. The two enums you met back in lesson 7 carry all of it:

enum Option<T> {
    Some(T),    // here's a value
    None,       // no value
}

enum Result<T, E> {
    Ok(T),      // success, here's the value
    Err(E),     // failure, here's the error of type E
}

Option<T> means “a T might be here, or might not.” Result<T, E> means “either a T (success) or an E (a typed reason it failed).” Because they are return types, the possibility of failure is printed right in the function’s signature — you cannot call a fallible function without the compiler reminding you it can fail.

The standard library leans on these everywhere:

There is no quiet null, and no exception you forgot to catch. The type system makes you decide what happens when the value is absent or the operation failed.

Handling a Result with match

The most explicit way to handle either enum is match, which forces you to write a branch for every variant. Here a function tries to parse a string into a number and the caller decides what each outcome means.

Hit Run — the first call succeeds, the second fails with a real parse error, and neither one crashes the program:

match: handle both outcomes by hand editable · real rustc
Open in Playground ↗ ready

It prints parsed age: 42, then not a number: invalid digit found in string. The failure is data you inspected and reacted to — not an exception that blew past you.

For the common cases there’s a richer vocabulary that reads more fluently than match: .map (transform the success value), .unwrap_or(default) (a fallback), .and_then (chain another fallible step), .ok_or (turn an Option into a Result). The general advice: reach for these helpers first, and drop to match only when they don’t quite fit.

Option, when there’s nothing wrong — just nothing there

Option<T> is for absence that isn’t an error: a list might have no tenth element, a lookup might find no match. Indexing with xs[10] would crash, but xs.get(10) hands you a calm None to deal with.

Option: absence without a crash editable · real rustc
Open in Playground ↗ ready

It prints Some(30), None, then -1 — the unwrap_or(-1) supplied a fallback for the missing element. Same shape as Result, minus the error payload.

The ? operator: propagation in one character

Handling every error at every level with match would be exhausting. Usually a function can’t fix a failure — it just wants to pass it up to whoever called it. Without help, that looks like this, repeated at every step:

let x = match a.parse::<i32>() {
    Ok(n) => n,
    Err(e) => return Err(e),   // bubble the error up
};

The ? operator collapses that whole block to one character. After a Result, ? means: if Ok, unwrap the value and continue; if Err, return early from this function with that error. The function’s return type must itself be a Result (or Option) for ? to have somewhere to return to.

A handy fact for the top level: main is allowed to return Result<(), Box<dyn Error>>, so you can use ? right inside main. The success path reads straight down the page; failures bubble out on their own.

? : the success path reads like there are no errors editable · real rustc
Open in Playground ↗ ready

It prints sum = 42. Both parses could have failed; ? would have returned the error early and main would have reported it. Change "40" to "forty" and run again — the program ends with an Err instead of a sum, no crash, no exception.

Portrait of computer scientist Philip Wadler
Philip Wadler · b. 1956 Co-designer of Haskell and a force behind the type-theory ideas Rust borrowed. Haskell's Maybe and Either types — values that encode 'might be absent' and 'might have failed' — are the direct ancestors of Rust's Option and Result.

panic! — for bugs, not for bad input

Everything above is for recoverable errors: expected, ordinary failures your program should handle gracefully. The other category is unrecoverable — a bug, a violated assumption, a state you designed against. For those Rust has panic!, which unwinds the stack and aborts the program.

The two most common ways to trigger a panic are .unwrap() and .expect("message") on an Option or Result. They mean: “I am certain this is Some/Ok; if I’m wrong, that’s a bug — crash.” The danger is using them on values that genuinely can be empty. Watch what happens when you .unwrap() a None:

unwrap on None — a real runtime panic editable · real rustc
Open in Playground ↗ ready

Hit Run and read the output. The program does not print never prints — instead it dies with:

thread 'main' panicked at src/main.rs:4:27:
called `Option::unwrap()` on a `None` value

That is a panic: no return value, no recovery, the program stops. The fix is almost always to not .unwrap() — handle the None with match, or propagate it with ?. If you find yourself sprinkling .unwrap() through real code, that’s a smell: there’s an Option or Result you should be carrying instead of asserting away.

So when is a panic right? When the situation truly cannot be handled and means your code is broken:

Library code should almost never panic on bad input from its caller — that’s exactly what returning a Result is for. Choosing between Result and panic! is a genuine design decision: is this a failure the caller should reasonably handle, or a bug that should halt the program loudly?

Why no exceptions is a deliberate trade-off

Exceptions have one real advantage: they keep the happy path free of error-handling clutter, because errors travel on an invisible channel. The cost is that the channel is invisible — you cannot tell from a function’s signature whether it might throw, what it might throw, or which lines could blow up. Error handling becomes a runtime property you discover by reading documentation, or by getting bitten in production.

Rust pays a small, constant tax instead: failure shows up in the type (-> Result<T, E>), and you acknowledge it at every call site — usually with a single ?. In exchange you get a property exceptions can’t offer: the set of functions that can fail is exactly the set whose return type says so, checked by the compiler. The ? operator is what makes this affordable. Without it, “errors as values” would mean a match at every step and nobody would tolerate it; with it, propagation costs one character and the happy path still reads top-to-bottom. The design is visible failure made cheap, rather than invisible failure made convenient.

Key takeaways

Other languages let a function lie: its signature says nothing, yet it can fail at any moment. Rust refuses the lie. If a function can fail, its type says so, and the compiler holds you to it.

The reward isn’t fewer errors — it’s that every error you could hit is written down, in the open, where a single ? can carry it onward. Next, lesson 11 takes this same insistence on visible truth into the hardest place to keep it: many things happening at once.