Beginner's Guide to anyhow and thiserror in Rust

Rust error handling can feel intimidating at first, especially when you want nice error messages without writing a lot of boilerplate. Two crates make this much easier:

  • thiserror helps you define clean, typed errors for libraries and reusable code.
  • anyhow helps you return ergonomic errors in application code, with great context.

In this post, we will build a tiny config loader using thiserror, then wire it into a main function using anyhow.

Add the Dependencies

Add these crates to your Cargo.toml:

[dependencies]
anyhow = "1"
thiserror = "2"

Use thiserror for a Clear Error Type

When you are writing a library or a module that other code will call, it is useful to return a typed error that callers can match on. thiserror lets you define this error type with minimal code.

Here is a small example that loads a server port from a file:

use std::fs;
use std::num::ParseIntError;
use thiserror::Error;

#[derive(Debug, Error)]
enum ConfigError {
    #[error("missing config file at {path}")]
    Missing { path: String },

    #[error("could not read config file at {path}: {source}")]
    ReadFile {
        path: String,
        source: std::io::Error,
    },

    #[error("invalid port: {source}")]
    InvalidPort { source: ParseIntError },
}

struct Config {
    port: u16,
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let contents = fs::read_to_string(path).map_err(|source| {
        if source.kind() == std::io::ErrorKind::NotFound {
            ConfigError::Missing {
                path: path.to_string(),
            }
        } else {
            ConfigError::ReadFile {
                path: path.to_string(),
                source,
            }
        }
    })?;

    let port = contents
        .trim()
        .parse::<u16>()
        .map_err(|source| ConfigError::InvalidPort { source })?;

    Ok(Config { port })
}

This gives you strongly typed errors with readable messages, and it still works with the ? operator.

Use anyhow in Application Code

In application code (like main), you often do not need to match on exact error types. You just want a useful error message with context. That is exactly what anyhow is for.

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = load_config("app.port")
        .with_context(|| "loading server port from app.port")?;

    run_server(config.port)
        .with_context(|| "starting server")?;

    Ok(())
}

fn run_server(port: u16) -> Result<()> {
    println!("Server running on http://localhost:{port}");
    Ok(())
}

If something fails, anyhow prints a nice error chain:

Error: loading server port from app.port

Caused by:
    missing config file at app.port

When to Use Which

  • Use thiserror in libraries or shared modules where callers might want to match on specific error variants.
  • Use anyhow in binaries and application code where you care more about good messages than precise types.
  • They work great together because thiserror implements std::error::Error, which anyhow can wrap automatically.

Quick Recap

  • thiserror gives you clean, typed errors with readable messages.
  • anyhow gives you easy error propagation plus context in apps.
  • Combine them: typed errors inside, ergonomic errors at the top.