Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8. Errors

Rust has no exceptions. A function that can fail says so in its return type, and the caller cannot ignore it without writing something that looks like ignoring it. That one decision shapes how Rust programs are put together, and it is worth understanding what it buys: you can read a signature and know what can go wrong, and you can read a call site and see where failure is handled.

Failures come in two kinds and Rust treats them differently. A bug is unrecoverable: an index out of range, an invariant broken, a case the programmer swore could not happen. The program stops. Everything else is expected: a file that is not there, input that is not a number, a network that is down. Those are values, and the type is Result.

8.1 Panic

panic! stops the current thread, prints a message and the line, and unwinds, running every destructor on the way out.

panic!("the world is inside out: {state}");

Most panics are not written by hand. They come from unwrap on an empty Option, from indexing past the end of a vector, from integer overflow in a debug build, from division by zero, or from an assert! that did not hold.

thread 'main' panicked at index_out_of_bounds.rs:3:21:
index out of bounds: the len is 3 but the index is 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Setting that environment variable gives you the call stack, usually all you need.

Panic when a condition means the program’s assumptions are broken, which includes most of what you would assert in C. Do not panic because a file was missing or a user typed something odd; that is ordinary life and it deserves a Result.

unwrap and expect are panics in disguise. They are fine in a first draft, in a test, and in a place where you have just established that the value is there. Prefer expect with a sentence saying what you were assuming, because that sentence is what you will read at three in the morning.

8.2 Result

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Handling one with match is always available:

use std::fs;

fn main() {
    match fs::read_to_string("numbers.txt") {
        Ok(text) => println!("{} bytes", text.len()),
        Err(e) => println!("could not read it: {e}"),
    }
}

and there are the same shorthand methods Option has. unwrap_or and unwrap_or_else supply a fallback. map transforms the success value and map_err transforms the error. ok() throws the error away and gives you an Option. is_ok() and is_err() ask without consuming.

Ignoring a Result entirely gets you a warning, because the return value is marked #[must_use]. When you genuinely do not care, say so with let _ = ..., a deliberate act rather than an oversight.

8.3 The Question Mark

Most of the time a function cannot handle the failure it just received and should hand it to its caller. That is what ? does:

let text = fs::read_to_string(path)?;

If the result is Ok, the value comes out and the program continues. If it is Err, the function returns that error immediately. It is the match you were going to write, in one character.

Two conditions. The enclosing function must return a Result itself, and the error type must convert into the function’s error type. The conversion is automatic where a From implementation exists, and that is what makes the next program work: read_to_string fails with an io::Error and parse fails with a ParseIntError, and both convert into a boxed error.

// Sum the numbers in a file, one per line.
use std::error::Error;
use std::fs;

fn sum_file(path: &str) -> Result<i64, Box<dyn Error>> {
    let text = fs::read_to_string(path)?;
    let mut total = 0;
    for line in text.lines() {
        let line = line.trim();
        if !line.is_empty() {
            total += line.parse::<i64>()?;
        }
    }
    Ok(total)
}

fn main() -> Result<(), Box<dyn Error>> {
    let path = std::env::args().nth(1).unwrap_or_else(|| "numbers.txt".to_string());
    println!("{} sums to {}", path, sum_file(&path)?);
    Ok(())
}
$ cargo run -q -p ch08 --bin read_file numbers.txt
numbers.txt sums to 42

$ cargo run -q -p ch08 --bin read_file /nope.txt
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
$ echo $?
1

Box<dyn Error> means “some error, whatever kind”, and it is the right choice for a program’s own internals, where you mostly want to report and stop. It is the wrong choice for a library, whose callers may want to tell one failure from another; for that, define the error type, as the next section does.

Note that main itself returns a Result. When it returns an Err, the runtime prints the error’s debug form and exits with status 1. That is a lot of correct behavior for one line of signature, and it is why main is written this way in most real programs.

? works on Option too, inside a function that returns Option, where it means “give up and return None”. Chapter 6’s expression evaluator used it that way. It does not convert between the two: use ok_or to turn a None into an error, and .ok() to turn an error into a None.

8.4 Your Own Error Type

An enum, one variant per thing that can go wrong, each carrying what a reader would need to fix it:

#[derive(Debug)]
enum ConfigError {
    MissingEquals { line: usize },
    EmptyKey { line: usize },
    BadNumber { line: usize, text: String },
}

To be an error type it needs to print itself. Display is the message a user sees, Debug is the one a programmer sees and is derived, and implementing std::error::Error is what lets it convert into a Box<dyn Error> and join the general machinery:

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::MissingEquals { line } => write!(f, "line {line}: no '=' found"),
            ConfigError::EmptyKey { line } => write!(f, "line {line}: empty key"),
            ConfigError::BadNumber { line, text } => {
                write!(f, "line {line}: {text:?} is not a number")
            }
        }
    }
}

impl std::error::Error for ConfigError {}

Now the parser. Note where each error is produced: ok_or turns the missing separator into one, map_err converts the standard library’s parse failure into one, and both are followed by ?.

fn parse(text: &str) -> Result<Vec<(String, i64)>, ConfigError> {
    let mut out = Vec::new();

    for (i, raw) in text.lines().enumerate() {
        let line = i + 1;
        let raw = raw.trim();
        if raw.is_empty() || raw.starts_with('#') {
            continue;
        }

        let (key, value) = raw.split_once('=').ok_or(ConfigError::MissingEquals { line })?;
        let key = key.trim();
        if key.is_empty() {
            return Err(ConfigError::EmptyKey { line });
        }

        let value = value.trim();
        let n = value.parse::<i64>().map_err(|_| ConfigError::BadNumber {
            line,
            text: value.to_string(),
        })?;

        out.push((key.to_string(), n));
    }

    Ok(out)
}
Ok([("width", 80), ("height", 24), ("depth", 3)])
error: line 1: no '=' found
error: line 1: empty key
error: line 1: "eighty" is not a number

The messages name the line number, the difference between an error report and an annoyance. Errors are for the person reading them; the type system only makes sure they arrive.

Writing Display by hand for every error type gets tiresome, and real projects use the thiserror crate to derive it, or anyhow to skip defining the type at all in programs where Box<dyn Error> would do. Chapter 10 covers adding dependencies. Learn the manual version first: both crates are generating what you just wrote.

8.5 Collecting Results

An iterator of Results can be collected two ways, and the choice says what should happen to the bad ones:

fn main() {
    let good = ["1", "2", "3"];
    let bad = ["1", "two", "3"];

    let parsed: Result<Vec<i64>, _> = good.iter().map(|s| s.parse::<i64>()).collect();
    println!("{parsed:?}");

    let parsed: Result<Vec<i64>, _> = bad.iter().map(|s| s.parse::<i64>()).collect();
    println!("{parsed:?}");

    let kept: Vec<i64> = bad.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{kept:?}");
}
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })
[1, 3]

Collecting into Result<Vec<_>, _> stops at the first failure and hands back that error. Collecting through filter_map with .ok() keeps what parsed and silently drops the rest. Both are one line, and the line says which policy you chose, more than most languages let you express.

8.6 A Working Rule

Return Result from anything that touches the outside world or interprets input someone else wrote. Use ? to carry failures up to a level that can do something about them, which is usually main or the top of a subsystem. Define an error enum when callers might branch on the kind of failure, and use Box<dyn Error> when they will not. Panic only for bugs. Never write a function that returns Option when the caller will want to know why it was empty.

Exercise 8-1. Give sum_file a real error type with variants for the read failing and for a line not parsing, and include the line number and its text in the second.

Exercise 8-2. parse stops at the first bad line. Write a version that returns Result<Vec<(String, i64)>, Vec<ConfigError>>, reporting every bad line at once. Which behavior would you want from a compiler, and which from a configuration file?

Exercise 8-3. What happens if a key appears twice? Decide on a policy, write it into the code, and add the error variant if you chose to reject it.

Exercise 8-4. Write a function that reads a file of numbers and returns their mean, and give it a sensible answer for an empty file. What type does that answer have?

Exercise 8-5. Replace one ? in sum_file with the match it stands for. Count the lines you added.