Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Errors, Collections, Traits and Concurrency

Error Handling with Result and ?

Recoverable errors are values of type Result<T, E>. Handle them with match, propagate them with ?, define your own error enum, and keep panics for real bugs.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A config parser that reads key=value lines, converts numbers, and reports exactly which line was wrong, using Result, the ? operator and a custom error type.

The idea

Rust separates two kinds of failure:

  • Unrecoverable bugs → panic! (the program stops). Indexing out of bounds or unwrap() on None panics.
  • Recoverable errors → return a Result<T, E>: Ok(value) or Err(error). The caller must decide what to do.

The ? operator is the workhorse: on Ok(v) it gives you v; on Err(e) it returns early from the current function with that error (converting it with From if needed). Functions using ? must themselves return Result (or Option).

For your own errors, define an enum with a variant per failure, implement Display for messages, and implement From<OtherError> so ? converts library errors automatically. Crates like thiserror (for libraries) and anyhow (for applications) remove the boilerplate.

The code

A custom error type, ? and From conversions
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    MissingEquals { line: usize },
    BadNumber { line: usize, source: ParseIntError },
    Missing(&'static str),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::MissingEquals { line } => write!(f, "line {line}: expected key=value"),
            ConfigError::BadNumber { line, source } => write!(f, "line {line}: not a number ({source})"),
            ConfigError::Missing(key) => write!(f, "missing required key `{key}`"),
        }
    }
}

#[derive(Debug)]
struct Config {
    port: u16,
    workers: u32,
}

fn parse(text: &str) -> Result<Config, ConfigError> {
    let mut port = None;
    let mut workers = None;
    for (i, raw) in text.lines().enumerate() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let (key, value) = line.split_once('=').ok_or(ConfigError::MissingEquals { line: i + 1 })?;
        let number = |v: &str| v.trim().parse::<u32>().map_err(|e| ConfigError::BadNumber { line: i + 1, source: e });
        match key.trim() {
            "port" => port = Some(number(value)? as u16),
            "workers" => workers = Some(number(value)?),
            _ => {} // ignore unknown keys
        }
    }
    Ok(Config {
        port: port.ok_or(ConfigError::Missing("port"))?,
        workers: workers.unwrap_or(4),
    })
}

fn main() {
    let inputs = [
        "# server\nport = 8080\nworkers = 8",
        "port = 8080",
        "port 8080",
        "port = 80\nworkers = many",
        "workers = 2",
    ];
    for text in inputs {
        match parse(text) {
            Ok(cfg) => println!("ok: {cfg:?}"),
            Err(e) => println!("error: {e}"),
        }
    }
}

Output

ok: Config { port: 8080, workers: 8 }
ok: Config { port: 8080, workers: 4 }
error: line 1: expected key=value
error: line 2: not a number (invalid digit found in string)
error: missing required key `port`

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • unwrap() everywhere. It turns every error into a crash; reserve it for tests, prototypes and truly impossible cases (or use expect("reason")).
  • Ignoring a Result. The compiler warns (unused Result); handle or propagate it.
  • Stringly-typed errors (Result<T, String>) in libraries; callers can't match on causes. Use an enum.
  • Losing the cause. Keep the source error (as BadNumber { source } does) so messages and logs stay useful.

Key points

  • panic! for bugs; Result<T, E> for expected failures.
  • ? unwraps Ok or returns the Err early.
  • Custom error enums + Display + From make ? ergonomic.
  • thiserror (libraries) and anyhow (apps) reduce boilerplate.

Further reading: Error handling.

Practice questions

What does the ? operator do in Rust?

Answer

Applied to a Result, it evaluates to the value inside Ok, or returns early from the enclosing function with the Err (converted via From into the function's error type). It also works on Option, returning None early.

When should Rust code panic instead of returning a Result?

Answer

When something indicates a bug or a broken invariant the program can't sensibly recover from (e.g. an impossible state), and in tests and quick prototypes. Expected failures like bad input, missing files or network errors should be Results.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?