🎓 A1Learner
🔍

Rust anyhow Patterns

anyhow patterns are the deeper tools for working with anyhow::Error in a real application. ensure! guards a rule in one line.

ensure! guards a rule

One line replaces an if, a message, and an early return.

ensure!(n <= 99, "level {n} is above the maximum of 99");

anyhow! builds an error value

When you need the error as a value, not an early return, anyhow! builds it from a message.

let err = anyhow!("level file is empty");

Walk every layer with chain()

chain() yields the top error first, then each cause under it, in order.

for cause in e.chain() {
    println!("cause: {cause}");
}

Find the root cause

chain() and root_cause() look through every layer of context at once.

if e.root_cause().downcast_ref::<std::num::ParseIntError>().is_some() {
    println!("(the root cause is a parse failure)");
}

The same error, two printings

{:#} gives one line for logs. {:?} gives the numbered report for humans.

println!("one line: {e:#}");
println!("full report:\n{e:?}");

Hand the typed error back

At a boundary, downcast by value gives the original typed error to code that matches on it.

if let Ok(io_err) = e.downcast::<std::io::Error>() {
    println!("kind: {:?}", io_err.kind());
}

Example

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

fn load_level(path: &str) -> Result<u32> {
    let text = fs::read_to_string(path)
        .with_context(|| format!("could not open {path}"))?;
    let n: u32 = text.trim().parse().context("level file did not hold a number")?;
    ensure!(n <= 99, "level {n} is above the maximum of 99");
    Ok(n)
}

fn run() -> Result<u32> {
    load_level("level.txt").context("could not load the starting level")
}

fn report(e: anyhow::Error) {
    if e.root_cause().downcast_ref::<std::num::ParseIntError>().is_some() {
        println!("(the root cause is a parse failure)");
    }
    println!("one line: {e:#}");
    println!("full report:\n{e:?}");
}

fn main() {
    fs::write("level.txt", "abc").unwrap();
    report(run().unwrap_err());
    fs::write("level.txt", "250").unwrap();
    println!("too big: {:#}", run().unwrap_err());
}

Key points

  • Each .context() call adds one more layer on top.
  • The error remembers the whole stack, newest first.

In this example, the level file holds abc, so parsing fails deep inside load_level. report first digs through both context layers with root_cause() and recognises the ParseIntError at the bottom. Then it prints the same error twice. With {:#} you get one line: your newest note, the older note, then the real cause. With {:?} you get the full report: the top note, then every cause numbered under Caused by:. Finally the file holds 250: it parses, but ensure! rejects it, and its message appears at the end of the chain.

Note

Downcasting is an escape hatch for boundaries.

If most of your code downcasts, the design wants typed errors instead — the next two lessons show the alternatives.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

You must react only when the deepest cause of an anyhow error is a ParseIntError. What reaches it?