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
- Exception: in languages like Python, Java, and C++, an exception is a special signal thrown when code hits a problem. It travels up the stack invisibly until some
catchblock handles it. If nothing does, the program crashes. Rust has no exceptions at all. - Panic: Rust’s emergency stop. When a program panics it prints a message and (by default) immediately terminates. Panics are for unrecoverable situations — logic bugs, violated assumptions — not for ordinary failures like a missing file.
- Propagate: to pass an error up to your caller instead of handling it yourself. If
fcallsgandgfails,fcan propagate the failure rather than deal with it. - The
?operator: a one-character shorthand for “if this is an error, return it from my function; otherwise unwrap the success value and keep going.” One of the most-typed symbols in real Rust.
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:
"42".parse::<i32>()returnsResult<i32, ParseIntError>— the number, or a parse error.vec.get(i)returnsOption<&T>— the item, ifiis a valid index.File::open(path)returnsResult<File, io::Error>— the open file, or an I/O error.HashMap::get(key)returnsOption<&V>— the value, if the key exists.
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:
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.
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.
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.
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:
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:
- An invariant of your own code was violated (you assumed a list was non-empty and it isn’t).
- An “impossible” branch was reached (you proved it can’t happen, but the compiler can’t see the proof).
- In tests, examples, and quick prototypes, where error UX doesn’t matter yet.
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
- Rust has no exceptions. A recoverable error is an ordinary value:
Option<T>for “might be absent,”Result<T, E>for “might fail with a typed error” — both visible in the function’s signature. - This idea descends from ML and Haskell (
MaybeandEither). Rust’s contribution was making it the only mechanism, so failure can’t hide. - Handle either enum with
match(full control) or the helper methods (map,unwrap_or,and_then,ok_or) for common cases. - The
?operator propagates an error in one character: onErr/None, return early; otherwise unwrap and continue.mainmay returnResult<(), Box<dyn Error>>so?works at the top level. It replaced the oldertry!macro in Rust 1.13. panic!(viaunwrap/expect) is for unrecoverable bugs, and it crashes the program.Resultis for expected, recoverable failures. Picking between them is a real design skill.
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.