Rust path · Types that do the work · Topic 7 of 12

Errors: Result and the ? operator

Not started

Rust separates two kinds of failure. **Recoverable** failures, such as a missing file, bad input or a refused connection, are values of type Result<T, E>, an enum that is either Ok(T) or Err(E). The possibility of failure is in the signature, and the compiler warns if you ignore a Result. **Unrecoverable** failures, bugs, are panic!s. The ? operator makes the first kind nearly as convenient as exceptions while keeping every exit point visible.

Why it matters: A function's signature tells you whether it can fail and how. The ? operator keeps the happy path readable, and From conversions let each layer speak its own error type.

  1. 1Result is an enum

    Result<T, E> is Ok(T) or Err(E). The compiler warns if you ignore one, because Result is marked must_use.

  2. 2? returns early on Err

    expr? gives the Ok value, or returns the Err from the current function, converting it with From::from on the way.

  3. 3panic is for bugs

    panic! unwinds the thread (or aborts, if configured). unwrap and expect panic too; keep them for cases that really can't happen.

  4. 4Libraries define types, applications box them

    A library usually returns its own error enum. An application often uses Box<dyn Error> or a crate such as anyhow to collect anything.

1

How it works

The explanation, with examples you can run.

Your own error type, and ?

use std::fmt;use std::num::ParseIntError; #[derive(Debug)]enum ConfigError {    Missing(&'static str),    BadNumber(ParseIntError),} impl fmt::Display for ConfigError {    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {        match self {            ConfigError::Missing(k) => write!(f, "missing key {k}"),            ConfigError::BadNumber(e) => write!(f, "bad number: {e}"),        }    }} impl std::error::Error for ConfigError {} impl From<ParseIntError> for ConfigError { // lets ? convert the error for us    fn from(e: ParseIntError) -> Self {        ConfigError::BadNumber(e)    }} fn workers(raw: Option<&str>) -> Result<u32, ConfigError> {    let text = raw.ok_or(ConfigError::Missing("workers"))?;    let n: u32 = text.trim().parse()?; // ParseIntError -> ConfigError via From    Ok(n)} fn main() -> Result<(), Box<dyn std::error::Error>> {    println!("{:?}", workers(Some(" 8 ")));    println!("{}", workers(None).unwrap_err());    println!("{}", workers(Some("eight")).unwrap_err());    let n = workers(Some("4"))?; // in main too, because main returns Result    println!("running {n} workers");    Ok(())}

Output: Ok(8), missing key workers, bad number: invalid digit found in string, running 4 workers.

expr? means: if expr is Ok(v), the value is v; if it is Err(e), return Err(From::from(e)) from the enclosing function right now. The From impl is what lets one function call code with different error types and still return its own. ? also works on Option in a function returning Option. ok_or converts an Option into a Result.

main can return Result<(), E> for any E: Debug. Returning Err prints the error and exits with a non-zero status. Box<dyn std::error::Error> accepts any error type, through a blanket From impl.

Choosing error types

  • Libraries usually return an enum like ConfigError, so callers can match on the cases. The thiserror crate derives the Display, Error and From boilerplate above.
  • Applications often care more about a good message than about matching. Box<dyn Error> or the anyhow crate collect any error and let you add context (.context("reading config")).
  • Implement Error::source() (or let thiserror do it) so a chain of causes can be printed, much like Go's wrapped errors.

Panics

panic!, unwrap(), expect(), array indexing out of bounds and integer overflow in debug builds all panic. By default a panic unwinds the current thread's stack, running Drop for every value as it goes, and ends the thread; in the main thread that ends the program. Setting panic = "abort" in a Cargo profile skips unwinding and makes binaries smaller. std::panic::catch_unwind exists for FFI boundaries and thread pools, not for ordinary error handling.

A useful habit: convert errors at boundaries. Inside a module, use a precise error type; at the edge of the program (an HTTP handler, main), turn it into a status code or a message for a human, and log the full chain of source() causes once.

Use expect("why this can't fail") rather than bare unwrap() when you do assert something: the message is the documentation.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.In a function returning Result<u32, ConfigError>, what does text.parse::<u32>()? need in order to compile?
  2. 2.Can ? be used in main?
  3. 3.When is unwrap() a reasonable choice?

0 of 3 right so far. Get all three to finish this topic.