iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Error Crates (anyhow / thiserror)

Rusts error story is structural: Result represents fallibility in the type system, the ? operator propagates errors with no boilerplate, and the standard libraries Error trait plus crates like thiserror/anyhow give you ergonomic typed errors. Default to thiserror for library code, anyhow for application code.

Result, ?, thiserror, anyhow, and the From trick

EXAMPLE
// Cargo.toml
// [dependencies]
// thiserror = "1"
// anyhow = "1"

use std::fs;
use std::io;
use thiserror::Error;
use anyhow::{Context, Result};

// ============================================================
// 1) Custom error type with thiserror — for LIBRARY code
// ============================================================
#[derive(Debug, Error)]
pub enum OrderError {
    #[error("order {0} not found")]
    NotFound(String),

    #[error("order {id} cannot move from {from} to {to}")]
    BadTransition { id: String, from: String, to: String },

    #[error("database error")]
    Db(#[from] sqlx::Error),         // From impl is generated automatically

    #[error("io error")]
    Io(#[from] io::Error),
}

// ============================================================
// 2) ? operator propagates errors and converts via From
// ============================================================
pub fn load_order_text(path: &str) -> Result<String, OrderError> {
    let raw = fs::read_to_string(path)?;        // io::Error -> OrderError via From
    if raw.is_empty() {
        return Err(OrderError::NotFound(path.into()));
    }
    Ok(raw)
}

// ============================================================
// 3) match with rich pattern fields
// ============================================================
fn explain(err: &OrderError) {
    match err {
        OrderError::NotFound(id) => println!("missing: {id}"),
        OrderError::BadTransition { id, from, to } =>
            println!("bad transition for {id}: {from} -> {to}"),
        OrderError::Db(e)  => println!("db: {e}"),
        OrderError::Io(e)  => println!("io: {e}"),
    }
}

// ============================================================
// 4) anyhow::Result for APPLICATION code — lossy but ergonomic
// ============================================================
fn main() -> Result<()> {
    let raw = fs::read_to_string("/etc/example/config.toml")
        .context("reading config")?;            // attach context to the error chain
    let cfg: Config = toml::from_str(&raw)
        .context("parsing config TOML")?;
    serve(&cfg).context("running server")?;
    Ok(())
}

// ============================================================
// 5) The error chain is printed top-down by default formatters
//    e.g. "running server: parsing config TOML: invalid TOML at line 8"
// ============================================================

// ============================================================
// 6) Boxing a heterogeneous error type when you cannot enumerate
// ============================================================
fn parse_any(s: &str) -> std::result::Result<u64, Box<dyn std::error::Error + Send + Sync>> {
    let n: u64 = s.trim().parse()?;
    Ok(n)
}

// ============================================================
// 7) Convert with map_err when From does not apply
// ============================================================
fn read_or_default(path: &str) -> Result<String, OrderError> {
    let raw = fs::read_to_string(path).map_err(|e| {
        // Add context-specific wrapping
        OrderError::Io(e)
    })?;
    Ok(raw)
}

// ============================================================
// 8) Decision tree
// ============================================================
//   Writing a library?       -> thiserror, typed enum, document each variant
//   Writing an application?  -> anyhow::Result + .context() at boundaries
//   Internal helper?         -> ? + propagate, let the caller decide
//   Recoverable vs fatal?    -> Result for recoverable; panic! for invariants

struct Config;
fn serve(_: &Config) -> Result<()> { Ok(()) }

Why it matters

`.context("doing X")?` on every fallible call at a logical boundary is what makes anyhow worth using — the resulting error chain reads like a stack trace written by a human, and locating the bug becomes "open the file mentioned in the deepest cause" instead of "diff your changes against last week".

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
use anyhow::{Context, Result};
fn run() -> Result<()> {
    let data = std::fs::read_to_string("input.txt")
        .context("reading input")?;
    println!("{} bytes", data.len());
    Ok(())
}
Try it Yourself »

Discussion

Loading…