Errors#

Rust has no exceptions. Recoverable failure is the Result<T, E> enum; absence is Option<T>; unrecoverable bugs use panic!. The ? operator propagates Err / None up the stack with the same readability as exceptions. thiserror and anyhow are the two crates the operator reaches for to make error types either precise or convenient.

/operators`.

Result and Option#

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

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    s.parse::<u16>()
}

fn first<T>(xs: &[T]) -> Option<&T> { xs.first() }

Use Result for “something might fail with a diagnosable reason” and Option for “something might be absent”.

The ? operator#

? unwraps Ok/Some and early-returns Err/None. The function’s return type must be Result/Option accordingly.

fn load_config(path: &str) -> std::io::Result<Config> {
    let text = std::fs::read_to_string(path)?;            // io::Error
    let cfg: Config = toml::from_str(&text).map_err(|e|
        std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
    Ok(cfg)
}

? calls From::from on the error, so different Err types coexist as long as conversions exist.

Custom errors (thiserror)#

For library code: define a typed error enum, derive thiserror::Error.

use thiserror::Error;

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),

    #[error("invalid TOML: {0}")]
    Toml(#[from] toml::de::Error),

    #[error("missing field: {0}")]
    MissingField(&'static str),
}

fn load(path: &str) -> Result<Config, ConfigError> {
    let text = std::fs::read_to_string(path)?;        // Io
    let cfg: Config = toml::from_str(&text)?;          // Toml
    Ok(cfg)
}

The #[from] annotation auto-generates impl From<std::io::Error>.

anyhow for applications#

For binary code where do not care about catching specific error kinds, anyhow::Error is a type-erased wrapper that prints a chain of contexts.

use anyhow::{Context, Result};

fn run() -> Result<()> {
    let path = std::env::var("CONFIG").context("CONFIG env var")?;
    let cfg  = load(&path).with_context(|| format!("loading {path}"))?;
    process(&cfg).context("processing")?;
    Ok(())
}

fn main() -> Result<()> { run() }

context and with_context chain extra information; the final error prints the whole chain.

unwrap and expect#

unwrap() and expect("msg") panic when the value is Err / None. Used in tests, in main for tutorials, and in code paths where an error genuinely cannot happen.

let port: u16 = "8080".parse().expect("hardcoded port should parse");

For production code prefer explicit handling. unwrap_or, unwrap_or_else, unwrap_or_default, and ok_or, ok_or_else, and_then, map cover most patterns.

let host = opts.host.unwrap_or_else(|| "localhost".to_string());
let port = opts.port.unwrap_or(8080);

let n: Result<u32, _> = "42".parse();
let v = n.unwrap_or(0);

panic!#

panic!(msg) aborts the current thread (by default unwinds the stack, runs Drop impls, propagates to the join handle). Use for programmer errors: violated invariants, impossible states, bugs you want the test suite to surface.

if !buffer.is_aligned() {
    panic!("buffer must be 4-byte aligned");
}

unreachable!() is the operator’s marker for code paths the type system cannot prove unreachable.

match version {
    1 => v1_handler(),
    2 => v2_handler(),
    _ => unreachable!("version validated upstream"),
}

debug_assert! only fires in debug builds; useful for hot paths.

debug_assert!(invariant_holds(&state));

Cargo.toml panic mode#

In Cargo.toml, pick abort (smaller binary, faster, no cleanup) or unwind (default, runs Drop, catchable with catch_unwind).

[profile.release]
panic = "abort"

Conversions across error types#

When ? needs to bridge between error types, implement From.

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

thiserror’s #[from] does this automatically; the operator only writes it by hand for tricky conversions.

Returning errors from main#

main can return Result<(), E> where E: Debug; the runtime prints the error with Debug and exits non-zero.

fn main() -> anyhow::Result<()> {
    run().context("startup failed")?;
    Ok(())
}

References#