Cogs and Levers A blog full of technical stuff

anyhow

Error handling in Rust is powerful — and sometimes verbose.

When you’re writing an application (not a reusable library), you often don’t care about building a perfectly structured error hierarchy.

You just want:

  • Clean propagation with ?
  • Useful context
  • A readable error message at the top
  • Minimal boilerplate

That’s exactly where anyhow fits.

What Problem Does anyhow Solve?

anyhow gives you a simple, ergonomic error type for applications.

Instead of defining custom enums everywhere, you use a single type:

anyhow::Result<T>

Internally, it can wrap any error type that implements:

std::error::Error + Send + Sync + 'static

It is designed for:

  • CLI tools
  • binaries
  • internal tooling
  • prototypes

It is not designed for library APIs.

That distinction matters.

Minimal Example

Let’s build a tiny CLI that reads a file and prints its contents.

Cargo.toml

[package]
name = "anyhow_demo"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"

main.rs

use std::fs;
use std::env;
use anyhow::{Result, Context};

fn main() -> Result<()> {
    let path = env::args()
        .nth(1)
        .context("expected a file path as first argument")?;

    let contents = fs::read_to_string(&path)
        .with_context(|| format!("failed to read file: {}", path))?;

    println!("{}", contents);

    Ok(())
}

Run it:

cargo run -- somefile.txt

If the argument is missing:

Error: expected a file path as first argument

If the file doesn’t exist:

Error: failed to read file: somefile.txt

Caused by:
    No such file or directory (os error 2)

Notice what happened:

  • We didn’t define a single custom error type.
  • We still got useful context.
  • We preserved the original error.

That’s the value.

What’s Actually Happening?

At its core, anyhow::Error is a type-erased error container.

It stores:

  • A “type-erased error” (internally boxed)
  • Optional context layers
  • Backtrace support (if enabled)

Result Alias

anyhow defines:

pub type Result<T> = std::result::Result<T, anyhow::Error>;

So this:

fn main() -> Result<()>

is just:

fn main() -> std::result::Result<(), anyhow::Error>

The Context Trait

This is where the crate becomes genuinely useful.

use anyhow::Context;

Adds:

  • .context("message")
  • .with_context(|| format!(...))

These wrap the existing error with additional information.

Importantly:

They do not destroy the underlying error.

They stack.

This is critical in real systems — you get:

  • High-level failure explanation
  • Low-level OS error preserved

Why It Feels “Rusty”

  • Works naturally with ?
  • Doesn’t invent new control flow
  • Leverages trait bounds instead of macros
  • Keeps error propagation explicit

It embraces Rust’s existing error model rather than replacing it.

That’s good design.

Where It Fits

anyhow is for applications.

Examples:

  • CLI tools
  • build scripts
  • internal utilities
  • one-off data processors
  • experimental tools

It is especially good when:

  • You’re composing lots of fallible operations.
  • You want to add context without ceremony.
  • You don’t care about exposing structured error types publicly.

If you’re writing something like a Substrate userland utility, this is perfect.

You care about correctness and clarity — not publishing an error taxonomy for other developers.

Where It Does Not Fit

Do not use anyhow in a public library API.

Why?

Because callers can’t match on your errors.

You’ve erased the type information.

Library authors should prefer structured errors (we’ll look at thiserror next).

Think of it like this:

  • anyhow = application boundary
  • thiserror = library boundary

Backtraces

anyhow supports backtraces when:

  • You compile with Rust’s backtrace support.
  • Or enable the appropriate feature flags.

If you export:

RUST_BACKTRACE=1

You’ll get stack traces layered with context.

This makes it surprisingly capable for production diagnostics.

Trade-offs

Let’s be honest.

Pros

  • Minimal boilerplate
  • Clean ergonomics
  • Excellent context layering
  • Integrates perfectly with ?

Cons

  • Type erasure
  • Not appropriate for library APIs
  • Slight heap allocation overhead (boxed error)

For CLI tools and applications, those trade-offs are almost always acceptable.

Should You Use It?

If you are writing a binary:

Yes.

If you are writing a library:

No.

That’s the rule.

anyhow reduces friction without compromising Rust’s safety model. It doesn’t hide failure — it just makes it easier to handle responsibly.

For application-level code, that’s exactly what you want.