Lesson 13 of 22ยท16 min

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 abc where a number was expected, a network timeout. These are values of type Result (or Option), 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 run

You 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:

MacroUse 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 x

When 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 a Result, 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 string

w.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
None

Without ?, 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 threads
Error: 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 aboutReturn Result and propagate with ?
A value that may be absent, not an errorReturn Option and propagate with ?
A sensible fallbackunwrap_or, unwrap_or_default, ...
A failure that means a bug or can't happenexpect("why it can't fail")
A quick prototype, test or exampleunwrap()
A broken invariant you detect yourselfpanic! 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 (or Option) 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! and todo! 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 a Result returns early with the Err, and after an Option returns early with None. It only works in functions that return the matching type, main included.
  • ? converts the error with From. Use map_err or a From implementation when the error types differ, and ok_or to turn an Option into a Result first.
  • Return a Result when 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.