Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errors and Recovery

A library error is part of a public contract; an application error is often a diagnostic story assembled while unwinding.

ThiserrorAnyhowTyped vs contextual failure

Thiserror: make failure inspectable

Consumer goal. Expose withdrawal failures that callers can handle by meaning rather than by parsing prose.

use thiserror::Error;

#[derive(Debug, Error)]
enum WithdrawError {
    #[error("amount must be greater than zero")]
    InvalidAmount,

    #[error("insufficient funds: requested {requested}, available {available}")]
    InsufficientFunds { requested: u64, available: u64 },
}

fn withdraw(balance: &mut u64, amount: u64) -> Result<(), WithdrawError> {
    if amount == 0 {
        return Err(WithdrawError::InvalidAmount);
    }
    if amount > *balance {
        return Err(WithdrawError::InsufficientFunds {
            requested: amount,
            available: *balance,
        });
    }

    *balance -= amount;
    Ok(())
}

fn main() {
    let mut balance = 100;

    match withdraw(&mut balance, 150) {
        Ok(()) => println!("new balance: {balance}"),
        Err(WithdrawError::InsufficientFunds { requested, available }) => {
            println!("cannot withdraw {requested}; only {available} remains")
        }
        Err(error) => println!("withdrawal failed: {error}"),
    }
}

Observed stdout.

cannot withdraw 150; only 100 remains

Boundary behavior. The caller chooses whether to recover from a specific variant, display the error, or propagate it.

Compile-time guarantee. Pattern matching exposes structured fields and can be exhaustive. Renaming the display message cannot silently break callers that match the enum.

Runtime boundary. Which failure occurs depends on input and state.

Tradeoff. A public enum is a strong stable contract. Adding a variant can break exhaustive downstream matches unless the API uses a non-exhaustive strategy.

Deep-dive question. What code does derive(Error) generate, and why does Thiserror avoid becoming visible in the public API’s types?

Anyhow: add meaning while failure travels upward

Consumer goal. Preserve a low-level JSON error while explaining what the application was trying to do.

use anyhow::{Context, Result};
use serde::Deserialize;

#[derive(Deserialize)]
#[allow(dead_code)]
struct Config {
    port: u16,
}

fn parse_config(input: &str) -> Result<Config> {
    serde_json::from_str(input)
        .context("configuration is not valid JSON")
}

fn main() {
    if let Err(error) = parse_config(r#"{"port":"many"}"#) {
        println!("{error:#}");
    }
}

Observed stdout with Anyhow 1.0.104 and Serde JSON 1.0.151.

configuration is not valid JSON: invalid type: string "many", expected u16 at line 1 column 14

Boundary behavior. The concrete Serde error becomes one cause in a chain; {:#} renders the chain compactly.

Compile-time guarantee. Result<T> keeps propagation explicit, but it does not expose a fixed set of error variants to the caller.

Runtime boundary. Recovery by semantic category is possible only through downcasting or earlier typed handling.

Tradeoff. Type erasure makes application plumbing easy and diagnostics rich, but it is usually a poor public contract for a reusable library.

Deep-dive question. How does Context preserve the original source while allowing lazy, application-specific messages?

Compare the architectural roles

QuestionThiserrorAnyhow
Primary audiencecallers of a reusable APImaintainers and users of an application
Public shapeconcrete enum or structerased anyhow::Error
Recoverymatch variants and fieldspropagate, display, or downcast
Evolution pressurevariants are API commitmentscontext text is less structural
Main valuestable machine-readable contractlow-friction diagnostic context

They often belong in the same system: typed errors within libraries, contextual errors near application orchestration and process boundaries.