Learn Rust Series (#50) - anyhow: Flexible Application-Level Errors & Context
Published on HivePostify by @scipio · Mon Sep 07 2026
Learn Rust Series (#50) - anyhow: Flexible Application-Level Errors & Context
What will I learn - You will learn how the anyhow crate handles errors in application code with almost no ceremony; - how anyhow::Result and anyhow::Error let ? accept any error type; - how .context() and .withcontext() attach human-readable context to an error; - how the anyhow!, bail! and ensure! macros create and return ad-hoc errors; - when to reach for anyhow versus thiserror, and what anyhow gives you over plain Box .
Requirements - A working modern computer running macOS, Windows or Ubuntu, with Cargo to add dependencies; - An installed Rust toolchain (via rustup, from rustup.rs); - The previous forty-nine episodes, especially custom errors, Box and thiserror; - The ambition to learn systems programming from the ground up.
Difficulty - Intermediate
Curriculum (of the Learn Rust Series): - [Learn Rust Series (#1) - Introduction to Rust](https://hive.blog/hive-196387/@scipio/learn-rust-series-1-introduction-to-rust) - [Learn Rust Series (#2) - Variables, Types, Functions](https://hive.blog/hive-196387/@scipio/learn-rust-series-2-variables-types-functions) - [Learn Rust Series (#3) - Ownership & Borrowing](https://hive.blog/hive-196387/@scipio/learn-rust-series-3-ownership-borrowing) - [Learn Rust Series (#4) - Control Flow & Pattern Matching](https://hive.blog/hive-196387/@scipio/learn-rust-series-4-control-flow-pattern-matching) - [Learn Rust Series (#5) - Structs & Enums](https://hive.blog/hive-196387/@scipio/learn-rust-series-5-structs-enums) - [Learn Rust Series (#6) - Error Handling](https://hive.blog/hive-196387/@scipio/learn-rust-series-6-error-handling) - [Learn Rust Series (#7) - Collections](https://hive.blog/hive-196387/@scipio/learn-rust-series-7-collections) - [Learn Rust Series (#8) - Traits & Generics](https://hive.blog/hive-196387/@scipio/learn-rust-series-8-traits-generics) - [Learn Rust Series (#9) - Modules & Crates](https://hive.blog/hive-196387/@scipio/learn-rust-series-9-modules-crates) - [Learn Rust Series (#10) - Lifetimes](https://hive.blog/hive-196387/@scipio/learn-rust-series-10-lifetimes) - [Learn Rust Series (#11) - Closures & the Iterator Trait](https://hive.blog/hive-196387/@scipio/learn-rust-series-11-closures-the-iterator-trait) - [Learn Rust Series (#12) - Smart Pointers: Box, Rc & RefCell](https://hive.blog/hive-196387/@scipio/learn-rust-series-12-smart-pointers-box-rc-refcell) - [Learn Rust Series (#13) - Concurrency: Threads, Channels, Arc & Mutex](https://hive.blog/hive-196387/@scipio/learn-rust-series-13-concurrency-threads-channels-arc-mutex) - [Learn Rust Series (#14) - Mini Project: A Command-Line To-Do App](https://hive.blog/hive-196387/@scipio/learn-rust-series-14-mini-project-a-command-line-to-do-app) - [Learn Rust Series (#15) - Trait Objects & Dynamic Dispatch](https://hive.blog/hive-196387/@scipio/learn-rust-series-15-trait-objects-dynamic-dispatch) - [Learn Rust Series (#16) - Static vs Dynamic Dispatch](https://hive.blog/hive-196387/@scipio/learn-rust-series-16-static-vs-dynamic-dispatch) - [Learn Rust Series (#17) - Associated Types vs Generic Parameters](https://hive.blog/hive-196387/@scipio/learn-rust-series-17-associated-types-vs-generic-parameters) - [Learn Rust Series (#18) - Operator Overloading with std::ops](https://hive.blog/hive-196387/@scipio/learn-rust-series-18-operator-overloading-with-stdops) - [Learn Rust Series (#19) - Deref, DerefMut & Deref Coercion](https://hive.blog/hive-196387/@scipio/learn-rust-series-19-deref-derefmut-deref-coercion) - [Learn Rust Series (#20) - Drop & Deterministic Destruction (RAII)](https://hive.blog/hive-196387/@scipio/learn-rust-series-20-drop-deterministic-destruction-raii) - [Learn Rust Series (#21) - From, Into, TryFrom & Idiomatic Conversions](https://hive.blog/hive-196387/@scipio/learn-rust-series-21-from-into-tryfrom-idiomatic-conversions) - [Learn Rust Series (#22) - Deriving Common Traits](https://hive.blog/hive-196387/@scipio/learn-rust-series-22-deriving-common-traits) - [Learn Rust Series (#23) - The Orphan Rule & Trait Coherence](https://hive.blog/hive-196387/@scipio/learn-rust-series-23-the-orphan-rule-trait-coherence) - [Learn Rust Series (#24) - Blanket Implementations & the Newtype Pattern](https://hive.blog/hive-196387/@scipio/learn-rust-series-24-blanket-implementations-the-newtype-pattern) - [Learn Rust Series (#25) - Marker Traits: Sized, Send, Sync & Copy](https://hive.blog/hive-196387/@scipio/learn-rust-series-25-marker-traits-sized-send-sync-copy) - [Learn Rust Series (#26) - Const Generics: Types That Depend on Values](https://hive.blog/hive-196387/@scipio/learn-rust-series-26-const-generics-types-that-depend-on-values) - [Learn Rust Series (#27) - Generic Associated Types & Lending Iterators](https://hive.blog/hive-196387/@scipio/learn-rust-series-27-generic-associated-types-lending-iterators) - [Learn Rust Series (#28) - Sealed Traits & Designing Stable APIs](https://hive.blog/hive-196387/@scipio/learn-rust-series-28-sealed-traits-designing-stable-apis) - [Learn Rust Series (#29) - Typestate Programming: State Machines in the Type System](https://hive.blog/hive-196387/@scipio/learn-rust-series-29-typestate-programming-state-machines-in-the-type-system) - [Learn Rust Series (#30) - Mini Project: A Generic Units-of-Measure Library](https://hive.blog/hive-196387/@scipio/learn-rust-series-30-mini-project-a-generic-units-of-measure-library) - [Learn Rust Series (#31) - Move Semantics Deep Dive](https://hive.blog/hive-196387/@scipio/learn-rust-series-31-move-semantics-deep-dive) - [Learn Rust Series (#32) - Interior Mutability: Cell & RefCell](https://hive.blog/hive-196387/@scipio/learn-rust-series-32-interior-mutability-cell-refcell) - [Learn Rust Series (#33) - Rc Internals: Reference Counting & Shared Ownership](https://hive.blog/hive-196387/@scipio/learn-rust-series-33-rc-internals-reference-counting-shared-ownership) - [Learn Rust Series (#34) - Arc: Thread-Safe Reference Counting & Its Cost](https://hive.blog/hive-196387/@scipio/learn-rust-series-34-arc-thread-safe-reference-counting-its-cost) - [Learn Rust Series (#35) - Weak References & Breaking Reference Cycles](https://hive.blog/hive-196387/@scipio/learn-rust-series-35-weak-references-breaking-reference-cycles) - [Learn Rust Series (#36) - Cow: Clone-on-Write for Borrow-or-Own APIs](https://hive.blog/hive-196387/@scipio/learn-rust-series-36-cow-clone-on-write-for-borrow-or-own-apis) - [Learn Rust Series (#37) - Pin & Self-Referential Structs](https://hive.blog/hive-196387/@scipio/learn-rust-series-37-pin-self-referential-structs) - [Learn Rust Series (#38) - PhantomData, Zero-Sized Types & Marker Lifetimes](https://hive.blog/hive-196387/@scipio/learn-rust-series-38-phantomdata-zero-sized-types-marker-lifetimes) - [Learn Rust Series (#39) - Variance: Covariance, Contravariance & Why It Matters](https://hive.blog/hive-196387/@scipio/learn-rust-series-39-variance-covariance-contravariance-why-it-matters) - [Learn Rust Series (#40) - Arena & Bump Allocation Patterns](https://hive.blog/hive-196387/@scipio/learn-rust-series-40-arena-bump-allocation-patterns) - [Learn Rust Series (#41) - Building Your Own Smart Pointer](https://hive.blog/hive-196387/@scipio/learn-rust-series-41-building-your-own-smart-pointer) - [Learn Rust Series (#42) - Drop Order, the Drop Check & Leak Safety](https://hive.blog/hive-196387/@scipio/learn-rust-series-42-drop-order-the-drop-check-leak-safety) - [Learn Rust Series (#43) - std::mem: swap, replace, take & forget](https://hive.blog/hive-196387/@scipio/learn-rust-series-43-stdmem-swap-replace-take-forget) - [Learn Rust Series (#44) - Higher-Ranked Trait Bounds & Lifetime Elision](https://hive.blog/hive-196387/@scipio/learn-rust-series-44-higher-ranked-trait-bounds-lifetime-elision) - [Learn Rust Series (#45) - Mini Project: A Doubly-Linked List, Safe then Unsafe](https://hive.blog/hive-196387/@scipio/learn-rust-series-45-mini-project-a-doubly-linked-list-safe-then-unsafe) - [Learn Rust Series (#46) - Result Combinators: map, maperr, andthen, okor](https://hive.blog/hive-196387/@scipio/learn-rust-series-46-result-combinators-map-maperr-andthen-okor) - [Learn Rust Series (#47) - Option Combinators & Null-Free Programming](https://hive.blog/hive-196387/@scipio/learn-rust-series-47-option-combinators-null-free-programming) - [Learn Rust Series (#48) - Custom Error Types & the std::error::Error Trait](https://hive.blog/hive-196387/@scipio/learn-rust-series-48-custom-error-types-the-stderrorerror-trait) - [Learn Rust Series (#49) - thiserror: Ergonomic Library Errors](https://hive.blog/hive-196387/@scipio/learn-rust-series-49-thiserror-ergonomic-library-errors) - [Learn Rust Series (#50) - anyhow: Flexible Application-Level Errors & Context](https://hive.blog/hive-196387/@scipio/learn-rust-series-50-anyhow-flexible-application-level-errors-context) (this post)
Learn Rust Series (#50) - anyhow: Flexible Application-Level Errors & Context
We reach the halfway mark of the planned series -- episode fifty, quite a milestone -- and we close the error-handling arc with anyhow, the natural counterpart to last episode's thiserror. If you remember the mental model I hammered on at the end of episode 49, it splits the world neatly in two: precise errors for libraries, flexible errors for applications. thiserror sits on the library side, where a caller needs a concrete, matchable enum to react differently to each failure mode. anyhow sits on the application side, where you mostly want to propagate an error up to main, attach some context along the way, and print a clean report when things go wrong.
Having said that, the two are not rivals -- they are partners, and mature projects reach for both. Today we learn what anyhow gives you, we keep proving (as always) that none of it is magic by writing the plain std equivalent right next to it, and we finish with a comparison to how Python and Go handle this same problem. No hand-waving allowed ;-)
Solutions to Episode 49 Exercises
Episode 49 was thiserror. Here are the three exercises worked out.
Exercise 1 asked you to rewrite AppError with #[derive(Error)], #[error] and #[from], then add a fresh variant wrapping std::io::Error with #[from] and confirm it propagates through ? with no maperr. In thiserror that is delightfully short: rust // requires the thiserror crate: shown for illustration, not compiled locally use thiserror::Error; use std::num::ParseIntError;
#[derive(Error, Debug)] enum AppError { #[error("could not parse a number")] Parse(#[from] ParseIntError), // generates From #[error("i/o failure")] Io(#[from] std::io::Error), // generates From }
fn load(path: &str) -> Result { let text = std::fs::readtostring(path)?; // io::Error -> AppError, no maperr Ok(text.trim().parse()?) // ParseIntError -> AppError, no maperr }
And here is the exact same thing hand-written in plain std, which is what the two #[from] attributes expand to -- this one compiles: rust use std::fmt; use std::num::ParseIntError;
#[derive(Debug)] enum AppError { Parse(ParseIntError), Io(std::io::Error) }
impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::Parse() => write!(f, "could not parse a number"), AppError::Io() => write!(f, "i/o failure"), } } } impl std::error::Error for AppError { fn source(&self) -> Option { match self { AppError::Parse(e) => Some(e), AppError::Io(e) => Some(e) } } } impl From for AppError { fn from(e: ParseIntError) -> AppError { AppError::Parse(e) } } impl From for AppError { fn from(e: std::io::Error) -> AppError { AppError::Io(e) } }
fn load(path: &str) -> Result { let text = std::fs::readtostring(path)?; // io::Error -> AppError via From Ok(text.trim().parse()?) // ParseIntError -> AppError via From }
fn main() { println!("{}", load("/definitely/missing").iserr()); } // true
Exercise 2 wanted a struct-form variant NotFound { key, table } whose #[error("...")] message interpolates both fields and reads like a sentence a user could act on. The generated Display is just an ordinary write! with the two named fields, so here is the compiled std shape of it: rust use std::fmt; #[derive(Debug)] enum StoreError { NotFound { key: String, table: String } } impl fmt::Display for StoreError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { StoreError::NotFound { key, table } => write!(f, "no record with key '{key}' in table '{table}'"), } } } fn main() { let e = StoreError::NotFound { key: "42".into(), table: "users".into() }; println!("{e}"); // no record with key '42' in table 'users' }
Exercise 3 asked for a catch-all #[error(transparent)] variant over Box , plus the hand-rolled equivalent, and a comparison of the line counts. The thiserror version is one attribute and one line; the hand-written version forwards Display and source to the inner error: rust use std::fmt; use std::error::Error;
#[derive(Debug)] enum AppError { Other(Box ) }
impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::Other(e) => write!(f, "{e}") } // forward inner message } } impl Error for AppError { fn source(&self) -> Option { match self { AppError::Other(e) => Some(e.asref()) } } } impl From > for AppError { fn from(e: Box ) -> AppError { AppError::Other(e) } }
fn main() { let inner: Box = "boom".into(); println!("{}", AppError::Other(inner)); // boom }
Eleven lines by hand versus two with transparent and #[from], and they behave identically. That gap -- twenty-ish lines of Display/Error/From collapsing into a handful of attributes -- is exactly the ceremony thiserror erases. Now let's look at the other half of the story, anyhow.
anyhow::Result and the magic of context
anyhow is an external crate you add to Cargo.toml, same as thiserror:
toml [dependencies] anyhow = "1"
Its core is anyhow::Error, a type that can hold any error, and the alias anyhow::Result , which is simply Result . Because anyhow::Error converts from any type that implements std::error::Error, the ? operator accepts every error kind you throw at it -- a std::io::Error, a ParseIntError, your own custom type -- with no From impls to write. That is the first big win: in application code you stop defining error enums altogether.
The signature move, though, and the reason people love the crate, is .context(). It wraps an error with a message explaining what you were trying to do when it failed. Raw errors are notoriously unhelpful in isolation -- "No such file or directory" tells you nothing about which file or why you wanted it. .context() fixes that by stacking a readable description on top, while keeping the original error as the cause underneath: rust // requires the anyhow crate: shown for illustration, not compiled locally use anyhow::{anyhow, bail, Context, Result};
fn loadconfig(path: &str) -> Result { let contents = std::fs::readtostring(path) .withcontext(|| format!("failed to read config from {path}"))?; if contents.trim().isempty() { bail!("config file {path} is empty"); } Ok(contents) }
fn parseport(s: &str) -> Result { let port: u16 = s.parse().context("port must be a valid number")?; if port Result { let port = parseport("8080")?; println!("using port {port}"); // using port 8080 Ok(()) }
Look at what is absent: no custom error type, no From impls, no maperr. The ? operator swallows a std::io::Error, a ParseIntError, and our own ad-hoc messages alike, and every one of them becomes an anyhow::Error. When loadconfig fails, the printed report reads like a stack of intentions -- "failed to read config from app.toml", and beneath it the operating system's "No such file or directory". That is the difference between an error you can debug at a glance and one you have to go spelunking for.
Note the two flavours: .context("literal") takes the message eagerly, while .withcontext(|| ...) takes a closure that only runs on the error path. Use the closure form whenever building the message costs something (a format! allocation, say), because on the happy path -- which is most of the time -- you do not want to pay for a string you will never show. It is the same eager-versus-lazy distinction we saw with unwrapor versus unwraporelse back in the combinators episodes ;-)
The convenience macros
Three macros make ad-hoc errors effortless. anyhow!("message with {value}") builds an anyhow::Error from a formatted string. bail!("...") is pure shorthand for return Err(anyhow!(...)) -- an early-return-with-error in one word. And ensure!(condition, "...") bails unless a condition holds, like assert! but returning an error in stead of panicking, which is exactly what you want when the input is bad but the program should not crash: rust // requires the anyhow crate: shown for illustration, not compiled locally use anyhow::{bail, ensure, Result};
fn checkedport(s: &str) -> Result { let port: u16 = s.parse()?; // ? converts ParseIntError ensure!(port >= 1024, "port {port} is reserved"); // bails if the check fails if port == 0 { bail!("port 0 is never valid"); } // explicit early return Ok(port) }
In plain std those macros are just early returns, and writing them out by hand shows precisely what the macros save you -- the whole return Err(...) dance shrinks to a single readable line: rust fn parseport(s: &str) -> Result { let port: u16 = s.parse().maperr(|| "not a number".tostring())?; if port }
impl fmt::Display for Contexted { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}: {}", self.context, self.source) } }
impl Error for Contexted { fn source(&self) -> Option { Some(self.source.asref()) } }
fn parseport(s: &str) -> Result > { s.parse:: ().maperr(|e| Box::new(Contexted { context: String::from("port must be a valid number"), source: Box::new(e), }) as Box ) }
fn main() -> Result > { let port = parseport("8080")?; println!("using port {port}"); // using port 8080 Ok(()) }
That is the entire idea. anyhow::Error is essentially a polished, industrial-strength version of this Box wrapper: it captures a backtrace (when RUSTBACKTRACE is set), it chains context cleanly so {:#} prints the whole "caused by" ladder, it formats a readable report for you, and -- a neat implementation detail -- it is a single word wide on the stack rather than the two words a Box fat pointer needs, because it stashes the vtable next to the data on the heap. Same shape you just wrote, better engineered.
A std context helper
You can even build a tiny .context()-style extension trait yourself, which is roughly what anyhow hands you for free. We covered extension traits and blanket impls back in episode 24, so this should look familiar: rust use std::fmt;
trait Contextable { fn context(self, msg: &str) -> Result ; }
impl Contextable for Result { fn context(self, msg: &str) -> Result { self.maperr(|e| format!("{msg}: {e}")) } }
fn main() { let r: Result = "x".parse:: ().context("parsing the count"); println!("{r:?}"); // Err("parsing the count: invalid digit found in string") }
That blanket impl adds a .context() method to every Result whose error type can be displayed -- which is nearly all of them. The lazy withcontext form, which only builds the message on the error path, is a small variation that takes a closure in stead of a &str: rust use std::fmt;
trait WithContext { fn withcontext String>(self, f: F) -> Result ; } impl WithContext for Result { fn withcontext String>(self, f: F) -> Result { self.maperr(|e| format!("{}: {e}", f())) // f() runs only on Err } }
fn main() { let path = "cfg.toml"; let r: Result = "x".parse:: ().withcontext(|| format!("reading {path}")); println!("{r:?}"); // Err("reading cfg.toml: invalid digit found in string") }
Tags: #stem#stemsocial#steemstem#rust#programming