Option, Result, and Saying Goodbye to null

Option, Result, and Saying Goodbye to null

Real techniques for living with Option day to day — unwrap, expect, map, and friends — plus Result, the sibling type Rust reaches for whenever something can fail and the reason matters. The moment error handling in Rust starts clicking.

13 min read4 learning objectives

What You'll Learn

  • Pull values out of Option<T> with unwrap, expect, unwrap_or, and map — and build the instinct for which one fits which moment
  • Meet Result<T, E>, the type Rust reaches for whenever something can fail and the reason behind it matters
  • Read real standard-library functions that return Result, like text parsing and file reading
  • Recognize panicking versus recovering as a deliberate choice — and start sensing which one a given situation actually calls for

Last lesson closed with a promise: combine enums with pattern matching, and you end up holding the pieces behind something that quietly replaces exception handling almost entirely. Time to assemble it. You already know one half — Option<T>, and how to match on one by hand. This lesson rounds that out with the methods people actually reach for day to day, introduces Option's sibling — the type Rust uses whenever something can fail and the reason genuinely matters — and leaves you able to read almost any error-handling code you'll meet out in the wild.

Getting the value out, the fast way: unwrap and expect

Matching is correct, but it isn't always what you feel like typing. Sometimes you just want the value — right now, no ceremony — because you're confident enough it's there to bet the program on it:

.unwrap() says, in effect, "give me what's inside, and if there's nothing there, crash this program immediately." .expect(message) does the exact same thing, but lets you leave a note for whoever — quite possibly future-you — reads the crash output at 2am. Both are entirely legitimate tools: in examples, in quick prototypes, in tests where "this absolutely must be Some, and if it isn't, I want to know the instant it happens" is exactly the right call. They're a far riskier default in code real people depend on staying up. Treat them as a deliberate, occasional choice — not a habit you reach for on autopilot.

Getting the value out, the considerate way: providing a fallback

More often, "crash the program" isn't actually the behavior you're after — "fall back to something sensible" is:

Quick side note on that second line, since it introduces something new: || String::from("Anonymous") is a closure — a tiny, anonymous, inline function. Read || expression as "a function that takes nothing and produces expression," and (you'll see this shape constantly from here on) |x| expression as "a function that takes x and produces expression." That's genuinely all you need to follow along below — we'll meet closures properly, and everything that makes them worth knowing in depth, in their own lesson.

.unwrap_or(value) hands back value the instant there's nothing inside — simple, and ideal when the fallback is cheap to build ahead of time, like a bare 0 or an empty string. .unwrap_or_else(closure) is the considerate cousin: it only ever runs the closure if it actually has to, which matters the moment building that fallback costs real work — allocating a String, querying a database, anything you'd genuinely rather skip unless there's truly no other option.

Transforming without unwrapping: map

.map() is one of those methods that feels like a small magic trick the first time it clicks. Read it as: "if there's a value in here, run it through this function and hand me back an Option of whatever that produces; if there's nothing, hand back None — untouched, without ever calling the function at all." Compare that to the four-step dance you'd otherwise write by hand — check, unwrap, transform, re-wrap — and .map() just... does all of it, correctly, in one motion. It's the exact same "do nothing to nothing, do something to something" shape you saw in plus_one last lesson — just spelled as a single chainable call instead of four lines of match.

A new enum, for when the reason matters: Result

Option<T> is the right tool exactly when a value is either there or it simply isn't, with nothing further to say about it. But plenty of operations don't merely "fail to produce a value" — they fail for a specific, knowable reason, and silently throwing that reason away would waste the most useful part of it. Reading a file might fail because it's missing, or because you lack permission, or because the disk itself is gone. Parsing text as a number fails because the text wasn't a number — and knowing precisely which of those happened is often the entire difference between a five-second fix and an hour of guessing in the dark.

For exactly this shape of problem, the standard library defines Option<T>'s sibling:

Same shape as Option<T>, with one crucial difference: the "didn't work" case carries data of its own. Ok(T) means "it worked, and here's your value"; Err(E) means "it didn't — and here, specifically, is why." That E is almost always a type built specifically to describe what went wrong: inspectable, often printable, sometimes detailed enough to show a user directly.

Result in the wild: turning text into numbers

.parse() is generic over what it returns — the compiler reads parse_age's return type, decides you want a u32, and produces a Result<u32, ParseIntError> to match. Feed it "28" and you get Ok(28). Feed it "hi mom" and you get back Err(ParseIntError { kind: InvalidDigit }) — a real, structured value carrying the precise reason things went sideways, not just a shrug.

unwrap and expect work on Result too

Same methods, same trade-off, same advice: reach for them when "if this fails, the program genuinely cannot continue, and I want to know immediately" is the correct call — which describes plenty of real situations (a config file that has to exist, a database connection nothing can run without). It's the wrong call for situations a careful program ought to recover from gracefully instead — and learning to instantly tell those two situations apart is, genuinely, most of what "being good at error handling" comes down to. We go all the way into that judgment call in a lesson coming up soon: panic versus Result, as a deliberate strategy.

A glimpse of where this is going

See that ? quietly riding along right after read_to_string(...)? That single character is the entire subject of a lesson later in this course — it takes the "check it, and if it's an error, stop here and hand the error upward" pattern (which the exercise below is about to have you write out by hand, more than once) and reduces the whole thing to one keystroke. Once it clicks, you'll genuinely wonder how error handling ever felt tedious.

Quick exercise

Write a function fn average(input: &str) -> Option<f64> that takes a space-separated string of numbers — something like "4 8 15 16 23 42" — parses every one, and returns their average; or None if the string is empty, or if even one piece fails to parse. .split_whitespace() and .parse() will get you the individual pieces; the genuinely interesting part is bailing out cleanly the moment one of them fails. (If match starts to feel clunky here, and ? feels like exactly what you want instead — you're right, and that's precisely the gap the upcoming lesson fills. Writing it the deliberate way once, by hand, is exactly what makes the shortcut land later.)

You can now live comfortably with "maybe" — and with "maybe, and here's specifically why not." In practice, that means you can write functions that are honest, right there in their signatures, about every way they might not hand you what you asked for: checked by the compiler, impossible to silently ignore, with nothing sneaking around behind your back. That honesty is a sizable part of why production Rust has a reputation for simply not falling over in the middle of the night. Next up: the workhorse collection you'll reach for constantly from here on — vectors.