Error Propagation and Panics
The previous lesson showed how to handle an Option or a Result right where
you get it, with match and helper methods. Often, though, the function that
hits a problem is not the right place to decide what to do about it. A
function that parses one line of a config file doesn't know whether the
caller wants to skip the line, show an error, or give up. It should hand the
error back and let the caller decide.
This lesson covers the ? operator, which does that handing back in a single
character, and the other side of Rust's error handling: panics, the errors a
program doesn't try to recover from. It ends with unwrap and expect, which
turn one into the other, and a guide to when each tool fits.
Two kinds of errors
Rust splits errors into two groups:
- Recoverable errors are things that can reasonably go wrong: a file
that doesn't exist, a user typing
abcwhere a number was expected, a network timeout. These are values of typeResult(orOption), and the caller decides what to do. - Unrecoverable errors are bugs or broken assumptions: an index past the end of an array, a value that "can't" be zero being zero. When one happens, the program panics: it prints a message and stops.
Most of this lesson is about the first group, but the second is simpler, so it comes first.
panic!
The panic! macro stops the program with a message. It takes the same
arguments as println!:
fn cell_index(row: usize, col: usize, width: usize) -> usize {
if col >= width {
panic!("column {col} is outside a grid {width} cells wide");
}
row * width + col
}
fn main() {
println!("{}", cell_index(1, 2, 4));
println!("{}", cell_index(1, 5, 4));
println!("never printed");
}The first call prints 6. The second one panics: Rust prints which thread
panicked, the file, line and column of the panic!, and your message. Then it
unwinds: it walks back up the call stack, dropping every value it owns
along the way so memory and files are cleaned up, and the program exits with
a non-zero status. never printed is never printed.
The note at the end says you can set RUST_BACKTRACE=1 to see the chain of
function calls that led to the panic, which helps when the panic happens deep
inside a library:
RUST_BACKTRACE=1 cargo runYou have already seen the standard library panic on its own: indexing past the
end of an array, integer overflow in a debug build, dividing an integer by
zero, or looking up a missing key with map[key]. They all go through the same
mechanism.
A few macros are shorthands for panic! with a fixed message:
| Macro | Use it for |
|---|---|
unreachable!() | A branch that your logic guarantees never runs |
todo!() | Code you haven't written yet (it still compiles) |
unimplemented!() | Something you don't plan to support |
assert!(cond) | Panic if cond is false |
assert_eq!(a, b) | Panic if a != b, printing both values |
todo!() is handy while sketching: a function whose body is todo!()
compiles, whatever its return type, so you can fill it in later.
unwrap and expect
unwrap() takes the value out of a Some or an Ok, and panics on None or
Err. It turns a recoverable error into an unrecoverable one:
fn main() {
let seats: u32 = "42".parse().unwrap();
let first = ['x', 'y'].first().unwrap();
println!("{seats} {first}");
}42 xWhen the value is missing, the panic message says what went wrong, but not why you expected it to be there:
fn main() {
let scores: Vec<u32> = Vec::new();
let best = scores.iter().max().unwrap();
println!("best score: {best}");
}expect(message) does the same as unwrap, but panics with your message. For
a Result, the error is printed after it:
fn main() {
let port: u16 = "80800"
.parse()
.expect("PORT should be a number between 0 and 65535");
println!("listening on {port}");
}A good expect message states what you assumed and why, so the person reading
the panic knows which assumption broke. Prefer expect over unwrap in any
code other people will run.
When is it fine to panic?
unwrap and expect are not bad. They are the right call when:
- A failure means a bug. If the value can only be missing because of a mistake in your own code, a panic is the honest answer, and there is nothing sensible for a caller to do anyway.
- You know more than the compiler.
"127.0.0.1".parse::<IpAddr>()returns aResult, but you wrote the string yourself and it's valid. - The program can't do its job without the value. A server with no configuration can only stop, so checking once at startup and panicking with a clear message is reasonable.
- Tests, examples and prototypes. A panic in a test is a test failure, which is what you want.
When a failure is something the caller could handle, like bad user input or
a missing file, return a Result instead.
Propagating errors by hand
Here is a function that parses a size written as "<width>x<height>", such
as "1920x1080". Three things can go wrong: there's no x, the width isn't a
number, or the height isn't. Handling each with match looks like this:
fn parse_size(text: &str) -> Result<(u32, u32), String> {
let (w, h) = match text.split_once('x') {
Some(parts) => parts,
None => return Err(format!("{text:?} has no 'x'")),
};
let width = match w.parse::<u32>() {
Ok(n) => n,
Err(e) => return Err(e.to_string()),
};
let height = match h.parse::<u32>() {
Ok(n) => n,
Err(e) => return Err(e.to_string()),
};
Ok((width, height))
}
fn main() {
println!("{:?}", parse_size("1920x1080"));
println!("{:?}", parse_size("1920"));
println!("{:?}", parse_size("1920xtall"));
}Ok((1920, 1080))
Err("\"1920\" has no 'x'")
Err("invalid digit found in string")It works, but most of it is the same pattern over and over: if it's Ok, keep
the value; if it's Err, return it from the function. That pattern is
propagating the error, and it's common enough to have its own operator.
The ? operator
Put ? after a Result and it does exactly that match for you: on Ok(value)
the whole expression becomes value, and on Err(e) the function returns
Err(e) immediately. Here is the same function with ?:
fn parse_size(text: &str) -> Result<(u32, u32), String> {
let (w, h) = text
.split_once('x')
.ok_or_else(|| format!("{text:?} has no 'x'"))?;
let width = w.parse::<u32>().map_err(|e| e.to_string())?;
let height = h.parse::<u32>().map_err(|e| e.to_string())?;
Ok((width, height))
}
fn main() {
println!("{:?}", parse_size("1920x1080"));
println!("{:?}", parse_size("1920"));
println!("{:?}", parse_size("1920xtall"));
}Ok((1920, 1080))
Err("\"1920\" has no 'x'")
Err("invalid digit found in string")Each step reads as the happy path, and a ? marks every place it can bail
out. The helpers from the last lesson do the converting: ok_or_else turns
the Option from split_once into a Result, and map_err turns each
ParseIntError into the function's String error type.
? is an early return, so it can only be used inside a function whose return
type can hold the error: a function returning Result for a Result, or
Option for an Option.
? also chains. The caller of parse_size can propagate further, and the
error travels up as far as it needs to:
fn parse_size(text: &str) -> Result<(u32, u32), String> {
let (w, h) = text
.split_once('x')
.ok_or_else(|| format!("{text:?} has no 'x'"))?;
let width = w.parse::<u32>().map_err(|e| e.to_string())?;
let height = h.parse::<u32>().map_err(|e| e.to_string())?;
Ok((width, height))
}
fn pixel_count(text: &str) -> Result<u64, String> {
let (width, height) = parse_size(text)?;
Ok(width as u64 * height as u64)
}
fn main() {
match pixel_count("3840x2160") {
Ok(n) => println!("{n} pixels"),
Err(e) => println!("bad size: {e}"),
}
match pixel_count("3840 by 2160") {
Ok(n) => println!("{n} pixels"),
Err(e) => println!("bad size: {e}"),
}
}8294400 pixels
bad size: "3840 by 2160" has no 'x'Only main decides what an error means here. Everything below it just passes
errors along.
? converts the error type
? does one more thing on the way out: it calls From::from on the error,
converting it to the function's error type if a conversion exists. That lets
you replace the map_err calls with a conversion written once. With a custom
error enum:
use std::num::ParseIntError;
enum SizeError {
MissingX,
BadNumber(ParseIntError),
}
impl From<ParseIntError> for SizeError {
fn from(error: ParseIntError) -> Self {
SizeError::BadNumber(error)
}
}
fn parse_size(text: &str) -> Result<(u32, u32), SizeError> {
let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?;
let width = w.parse::<u32>()?;
let height = h.parse::<u32>()?;
Ok((width, height))
}
fn main() {
for input in ["800x600", "800", "-800x600"] {
match parse_size(input) {
Ok((w, h)) => println!("{w} by {h}"),
Err(SizeError::MissingX) => println!("{input}: no 'x'"),
Err(SizeError::BadNumber(e)) => println!("{input}: {e}"),
}
}
}800 by 600
800: no 'x'
-800x600: invalid digit found in stringw.parse::<u32>()? produces a ParseIntError, the function returns a
SizeError, and ? bridges the two with the From implementation. From is
a trait; the traits lessons explain impl ... for ... in full.
? with the standard library's I/O errors
Most I/O functions in std return std::io::Result<T>, which is short for
Result<T, std::io::Error>. ? works on them like on any other Result:
use std::fs;
use std::io;
fn longest_line(path: &str) -> io::Result<usize> {
let text = fs::read_to_string(path)?;
Ok(text.lines().map(|line| line.len()).max().unwrap_or(0))
}
fn main() {
match longest_line("Cargo.toml") {
Ok(n) => println!("longest line: {n} bytes"),
Err(e) => println!("error: {e}"),
}
match longest_line("missing.txt") {
Ok(n) => println!("longest line: {n} bytes"),
Err(e) => println!("error: {e}"),
}
}longest line: 17 bytes
error: No such file or directory (os error 2)(The first number depends on your Cargo.toml, and the error text on your
operating system.)
? with Option
In a function that returns an Option, ? works on Option values: Some(v)
becomes v, and None makes the function return None right away:
fn initials(full_name: &str) -> Option<String> {
let mut words = full_name.split_whitespace();
let first = words.next()?.chars().next()?;
let last = words.last()?.chars().next()?;
Some(format!("{first}.{last}."))
}
fn main() {
println!("{:?}", initials("Grace Brewster Hopper"));
println!("{:?}", initials("Ada Lovelace"));
println!("{:?}", initials("Ferris"));
println!("{:?}", initials(""));
}Some("G.H.")
Some("A.L.")
None
NoneWithout ?, each of those four steps would need its own match or let else. Note that "Ferris" gives None because after words.next() takes
the only word, words.last() has nothing left.
? doesn't mix the two: using it on an Option in a function that returns a
Result (or the other way round) is a compile error. Convert first, with
ok_or or ok_or_else to go from Option to Result, or .ok() to go from
Result to Option, then apply ?.
Returning a Result from main
main can return a Result too, which lets you use ? in it. If main
returns an Err, Rust prints it with {:?} to standard error and exits with
status 1:
use std::num::ParseIntError;
fn main() -> Result<(), ParseIntError> {
let threads: u8 = "8".parse()?;
println!("using {threads} threads");
let retries: u8 = "three".parse()?;
println!("retrying {retries} times");
Ok(())
}using 8 threadsError: ParseIntError { kind: InvalidDigit }The first block is the program's output, the second is what appears on
standard error. When main calls functions with different error types, a
common return type is Result<(), Box<dyn std::error::Error>>, which can hold
any error type. Box<dyn ...> is covered in the trait objects lesson.
Which one to use
| You have... | Use |
|---|---|
| A failure the caller should decide about | Return Result and propagate with ? |
| A value that may be absent, not an error | Return Option and propagate with ? |
| A sensible fallback | unwrap_or, unwrap_or_default, ... |
| A failure that means a bug or can't happen | expect("why it can't fail") |
| A quick prototype, test or example | unwrap() |
| A broken invariant you detect yourself | panic! or assert! |
Library code in particular should rarely panic on input it was given: the library can't know whether the caller would rather retry, skip or give up.
Common mistakes
Using ? in a function that returns ()
fn main() {
let level: u8 = "7".parse()?;
println!("level {level}");
}? needs somewhere to return the error to, and main here returns ().
Either make the function return a Result (as in the section on main
above), or handle the error where it happens with match, unwrap_or or
expect.
Using ? on an Option in a function that returns a Result
fn first_word_len(text: &str) -> Result<usize, String> {
let word = text.split_whitespace().next()?;
Ok(word.len())
}
fn main() {
println!("{:?}", first_word_len("hello world"));
}None has no error value to put in the Err, so Rust can't convert it. Say
what the error is with ok_or or ok_or_else before the ?:
.next().ok_or("empty text")?. That works even though the error is a
&str and the function returns a String error, because ? converts it with
From, as shown in the section on converting errors.
An error type ? can't convert
fn parse_age(text: &str) -> Result<u8, String> {
let age = text.trim().parse::<u8>()?;
Ok(age)
}
fn main() {
println!("{:?}", parse_age("31"));
}parse fails with a ParseIntError, but the function promises a String
error, and there's no From<ParseIntError> for String. Convert it yourself
with .map_err(|e| e.to_string())?, or use an error enum with a From
implementation as shown above.
Unwrapping an Err you didn't expect
fn main() {
let input = " 12";
let quantity: u32 = input.parse().unwrap();
println!("{quantity}");
}parse doesn't skip whitespace, so " 12" is not a number, and unwrap
turns that into a panic. The fix is not a better unwrap but handling the
case: trim() the input first, and since the input comes from outside the
program, return or match on the Result instead of unwrapping it.
Summary
- Recoverable errors are
Result(orOption) values. Unrecoverable errors are panics, which print a message, unwind the stack and stop the program. panic!panics with a message;assert!,assert_eq!,unreachable!andtodo!are shorthands for common cases.unwrap()takes the value out or panics.expect("...")does the same with your message; prefer it, and say why the value should be there.?after aResultreturns early with theErr, and after anOptionreturns early withNone. It only works in functions that return the matching type,mainincluded.?converts the error withFrom. Usemap_error aFromimplementation when the error types differ, andok_orto turn anOptioninto aResultfirst.- Return a
Resultwhen the caller could handle the failure; panic when a failure means a bug.
Practise what you read
Challenges that use the ideas from this lesson.