Schrödinger's Result
In Schrödinger's thought experiment,1 Wikipedia, Schrödinger's cat: the thought experiment, from 1935. a cat sealed in a box is both alive and dead until someone opens it to look.
Rust's Result<T, E>2 The Rust book, Recoverable Errors with Result: the chapter this page follows, in Brown University's edition.
is the same box. A function that can fail returns either Ok, holding a
value, or Err, holding an error. Calling
.unwrap()
bets that the cat is alive. If it is not, the program crashes.
1 unwrap and expect
Every way of opening the box gets the value out when there is one. What sets them
apart is what they do when there is none.
With unwrap there is no plan B.3 The Rust book, Shortcuts for Panic on Error: unwrap and expect.
On an Err the program panics and stops, printing the error and the
line that called unwrap.
The example below parses a cat's lives from a string into a u8.
parse4 Rust's standard library, str::parse: turning a string into a number.
returns a Result, explained in the next section.
"9" gives Ok(9), but
"nine" gives an Err, so the second line panics:
let lives: u8 = "9".parse().unwrap();// succeeds
let lives: u8 = "nine".parse().unwrap();// panics
expect panics the same way, but with a message of your choosing in place
of the generic one:
let lives: u8 = "nine".parse().expect("lives should be a number");
expect replaces the generic one.
Errors that cannot happen sometimes do. In November 2025 a file that feeds
Cloudflare's bot detection grew past its limit of 200 features, and the code that
read it called unwrap on the result. The unwrap panicked,
and many sites behind Cloudflare went down.5 Cloudflare, Cloudflare outage on November 18, 2025: the incident report.
2 Result and Option
A Result is always one of two things:6 Rust's standard library, Result: the methods on Result, with examples.
Ok with the value, or Err with the error. A type like that
is called an enum, and Ok and Err are its variants.
Parsing a cat's lives gives a Result<u8, ParseIntError>:
Ok with a u8 if it worked, Err with a
ParseIntError if it failed:
let lives: Result<u8, ParseIntError> = "9".parse();// Ok(9)
let lives: Result<u8, ParseIntError> = "nine".parse();// Err: an invalid digit
unwrap, from the previous section, takes the value out of
the Ok:
let lives: Result<u8, ParseIntError> = "9".parse();// Ok(9)
let n: u8 = lives.unwrap();// 9
The first type in the angle brackets is the value's, the second the error's. They
can be any types,7 The Rust book, Data Types: the types Rust has built in.
so different functions can fail with different errors. For example, reading a file
as text gives a Result<String, io::Error>.
Option8 Rust's standard library, Option: the methods on Option, with examples.
is the box without an error: Some holding a value, or
None holding nothing. For example, find returns
Some with the position of a letter in a word, counting from 0, or
None when the letter is missing:
let at: Option<usize> = "nine".find('e');// Some(3)
let at: Option<usize> = "nine".find('x');// None
In memory, a Result<u8, ParseIntError> takes two bytes. The first
byte, the tag, says which variant it is: 0 for Ok, 1 for
Err. The second byte holds what that variant carries. For
Ok it is the number. For Err it is a code for the kind of error, and an empty string is code 0.
So "0" and "" leave the same second byte, and only the tag
tells them apart.
ok turns the Result into an Option<u8>,
which also takes two bytes. It keeps the number and drops the error. Here the tag is
1 for Some and 0 for None. A None carries
nothing, so its second byte goes unused.
3 match, if let and let-else
A match9 The Rust book, The match Control Flow Construct: on why every case must be covered.
opens the box, and Rust forces you to handle every outcome. The
::<u8> below is a turbofish. The footnote links to
something beautiful. Do follow it.10 The Bastion of the Turbofish: a test in Rust's compiler that guards the syntax.
match input.parse::<u8>() {
Ok(n) => println!("parsed {n}"),
Err(e) => println!("error: {e}"),
}
When only one outcome matters, if let handles it and ignores the other.
let-else does the reverse: it keeps the value, and returns
from the function when there is none.
if let Ok(lives) = input.parse::<u8>() {
println!("{lives} lives left");
}
let Ok(lives) = input.parse::<u8>() else {
return;
};
4 unwrap_or and its variants
There is often a sensible value to fall back on when parsing
fails.11 Rust's standard library, Extracting contained values: every way to take the value out.
unwrap_or takes the fallback itself. unwrap_or_default uses
the type's default, which for a number is 0. unwrap_or_else takes a small
function that runs only on an error, with the error as its input.
let lives = input.parse::<u8>().unwrap_or(9);
let lives = input.parse::<u8>().unwrap_or_default();
let lives = input.parse::<u8>().unwrap_or_else(|e| {
eprintln!("{e}, assuming nine");
9
});
The first and the last differ in when the fallback is computed.
unwrap_or's argument is evaluated before the call, whether it is needed or
not. A fallback that is costly to compute belongs in unwrap_or_else, which
computes it only on an error.
5 map, map_err, and_then and ok
The value can be worked on without taking it out of the box. map changes
the value in an Ok and leaves an Err alone.
map_err changes the error in an Err and leaves an
Ok alone. and_then runs a step that can itself fail.
ok turns a Result into an Option, dropping the
error.
The function below spends one of a cat's lives. It turns a parse error into text and refuses a cat with no lives left. Then it subtracts one:
fn spend_a_life(input: &str) -> Result<u8, String> {
input
.parse::<u8>()
.map_err(|e| e.to_string())
.and_then(|n| match n {
0 => Err("no lives left".to_string()),
_ => Ok(n),
})
.map(|n| n - 1)
}
A chain like this can be drawn as a railway with two tracks.12 Scott Wlaschin, Railway Oriented Programming: the picture, first drawn for F#. Each step works on one track and lets the other pass. Once a value has switched to the error track, it stays there.
spend_a_life() gives 6 The ? operator
Often an error should be handled by whoever called the function, not by the function
itself. The ? operator,13 The Rust book, The ? Operator Shortcut: on passing errors up.
written after a Result, passes it up. On Ok it takes the value
out and carries on. On Err it returns the error from the function right
away.
fn lives(path: &str) -> Result<u8, LivesError> {
let text = fs::read_to_string(path)?;
let lives = text.trim().parse()?;
Ok(lives)
}
Each ? stands for a match:14 The Rust Reference, The try propagation expression: what ? means, exactly.
let text = match fs::read_to_string(path) {
Ok(text) => text,
Err(e) => return Err(From::from(e)),
};
fn lives(path: &str) -> Result<u8, LivesError> {
let text = fs::read_to_string(path)?;
let lives = text.trim().parse()?;
Ok(lives)
}
fn main() {
match lives("cat.txt") {
Ok(n) => println!("{n} lives left"),
Err(e) => println!("no cat: {e}"),
}
}
From::from lets one function return two kinds of error. Reading the file
fails with an io::Error, parsing with a ParseIntError, and
both are converted into the function's own LivesError on the way out.
Even main can return a Result. Its error goes up to the
operating system as exit code 1.
fn main() -> Result<(), ParseIntError> {
let lives: u8 = "nine".parse()?;
println!("{lives} lives left");
Ok(())
}
Every program hands back such a code when it ends. 0 means success and anything else a
failure. On Linux, echo $? shows the last one:
jani@janix ~/lives % ./lives
Error: ParseIntError { kind: InvalidDigit }
jani@janix ~/lives % echo $?
1
| 0 | success |
| 1 | a general error, which is what an Err from main gives |
| 2 | the command was used wrongly, by convention |
| 101 | a Rust panic |
| 126 | the file cannot be run |
| 127 | no such command |
| 128 + n | stopped by signal n, so 130 after Ctrl+C |
7 Custom error types
LivesError is an enum with a variant for each way the function can
fail, so a caller can match on what went wrong. The From impls are what
? calls:
#[derive(Debug)]
enum LivesError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for LivesError {
fn from(e: io::Error) -> Self {
LivesError::Io(e)
}
}
A second From impl does the same for ParseIntError. A
Display impl lets the error be printed, and the
Error trait15 Rust's standard library, Error: the trait every error type shares.
lets it work with other libraries' errors.
This is a lot of boilerplate,16 Wikipedia, Boilerplate code: code repeated with little change.
which the anyhow17 anyhow: one error type for any error.
crate lets you skip. Any error fits into its one error type, and context
adds a line saying what failed:
fn lives(path: &str) -> anyhow::Result<u8> {
let text = fs::read_to_string(path).context("can't open the box")?;
let lives = text.trim().parse().context("can't count the lives")?;
Ok(lives)
}
8 When to panic
The Rust book's rule of thumb:18 The Rust book, To panic! or Not to panic!: guidelines for choosing.
panic when continuing would be a bug. Return a
Result when failure is something the caller can expect and handle, like a
missing file or a typo in the input.
u8.