ExamplesBy LevelBy TopicLearning Paths
298 Intermediate

298: The anyhow Pattern — Boxed Errors

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "298: The anyhow Pattern — Boxed Errors" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Application code (as opposed to library code) often doesn't need to classify errors precisely — it just needs to propagate them to a top-level handler that logs or displays them. Key difference from OCaml: 1. **Type erasure**: `Box<dyn Error>` erases the concrete error type at runtime; the dynamic dispatch allows uniform handling of any error.

Tutorial

The Problem

Application code (as opposed to library code) often doesn't need to classify errors precisely — it just needs to propagate them to a top-level handler that logs or displays them. Defining a custom error enum for every function that calls multiple libraries is over-engineering. The anyhow pattern uses Box<dyn Error + Send + Sync> as a universal error container — any error can be boxed and propagated without defining wrapper types.

🎯 Learning Outcomes

  • • Understand Box<dyn Error + Send + Sync> as a type-erased error container
  • • Implement a Context wrapper that adds descriptive messages to errors
  • • Recognize when to use anyhow (applications) vs thiserror (libraries)
  • • Traverse the error chain via source() to display full context
  • Code Example

    type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
    
    fn parse_port(s: &str) -> AnyResult<u16> {
        let n: u16 = s.parse()?;  // auto-boxed
        if n == 0 { return Err("port cannot be zero".into()); }
        Ok(n)
    }

    Key Differences

  • Type erasure: Box<dyn Error> erases the concrete error type at runtime; the dynamic dispatch allows uniform handling of any error.
  • Context chaining: anyhow (and this pattern) preserves the original error as source() — the context wraps but doesn't replace.
  • Application vs library: anyhow/boxed errors are appropriate for applications; libraries should use precise error types via thiserror.
  • Thread safety: Send + Sync bounds enable using errors across async tasks and threads — essential for concurrent applications.
  • OCaml Approach

    OCaml uses result with string errors for simple cases, or Printexc for exceptions. The Error_monad from Tezos and Lwt's error handling provide richer composable errors, but there is no standard Box<dyn Error> equivalent:

    (* Simple approach: use string as universal error *)
    type 'a result_with_context = ('a, string) result
    
    let with_context ctx = function
      | Error e -> Error (ctx ^ ": " ^ e)
      | Ok v -> Ok v
    

    Full Source

    #![allow(clippy::all)]
    //! # anyhow-style Boxed Errors
    //!
    //! `Box<dyn Error + Send + Sync>` is a universal error container — the anyhow pattern.
    
    use std::error::Error;
    use std::fmt;
    
    /// Type alias for ergonomics (what anyhow::Result is essentially)
    pub type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
    
    /// A simple context wrapper
    #[derive(Debug)]
    struct WithContext {
        context: String,
        source: Box<dyn Error + Send + Sync>,
    }
    
    impl fmt::Display for WithContext {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.context)
        }
    }
    
    impl Error for WithContext {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(self.source.as_ref())
        }
    }
    
    /// Extension trait for adding context (like anyhow's .context())
    pub trait ResultExt<T> {
        fn context(self, msg: &str) -> AnyResult<T>;
    }
    
    impl<T, E: Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
        fn context(self, msg: &str) -> AnyResult<T> {
            self.map_err(|e| {
                Box::new(WithContext {
                    context: msg.to_string(),
                    source: Box::new(e),
                }) as Box<dyn Error + Send + Sync>
            })
        }
    }
    
    /// Parse port number
    pub fn parse_port(s: &str) -> AnyResult<u16> {
        let n: u16 = s.parse()?; // ? boxes any error
        if n == 0 {
            return Err("port cannot be zero".into()); // .into() on &str!
        }
        Ok(n)
    }
    
    /// Load configuration
    pub fn load_config(port_str: &str, host: &str) -> AnyResult<String> {
        let port = parse_port(port_str).map_err(|e| {
            Box::new(WithContext {
                context: "invalid port number".to_string(),
                source: e,
            }) as Box<dyn Error + Send + Sync>
        })?;
        if host.is_empty() {
            return Err("empty hostname".into());
        }
        Ok(format!("{}:{}", host, port))
    }
    
    /// String literal as error
    pub fn require_non_empty(s: &str) -> AnyResult<&str> {
        if s.is_empty() {
            Err("value cannot be empty".into())
        } else {
            Ok(s)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_load_config_ok() {
            let result = load_config("8080", "localhost");
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), "localhost:8080");
        }
    
        #[test]
        fn test_parse_port_bad() {
            assert!(parse_port("abc").is_err());
        }
    
        #[test]
        fn test_empty_host() {
            assert!(load_config("8080", "").is_err());
        }
    
        #[test]
        fn test_port_zero() {
            assert!(parse_port("0").is_err());
        }
    
        #[test]
        fn test_string_literal_error() {
            let result = require_non_empty("");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_context_preserves_source() {
            let pe: Result<u16, std::num::ParseIntError> = "abc".parse();
            let result = pe.context("parsing port");
            assert!(result.is_err());
            let e = result.unwrap_err();
            assert!(e.source().is_some());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_load_config_ok() {
            let result = load_config("8080", "localhost");
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), "localhost:8080");
        }
    
        #[test]
        fn test_parse_port_bad() {
            assert!(parse_port("abc").is_err());
        }
    
        #[test]
        fn test_empty_host() {
            assert!(load_config("8080", "").is_err());
        }
    
        #[test]
        fn test_port_zero() {
            assert!(parse_port("0").is_err());
        }
    
        #[test]
        fn test_string_literal_error() {
            let result = require_non_empty("");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_context_preserves_source() {
            let pe: Result<u16, std::num::ParseIntError> = "abc".parse();
            let result = pe.context("parsing port");
            assert!(result.is_err());
            let e = result.unwrap_err();
            assert!(e.source().is_some());
        }
    }

    Deep Comparison

    OCaml vs Rust: anyhow Pattern

    Pattern: Universal Error Container

    Rust

    type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
    
    fn parse_port(s: &str) -> AnyResult<u16> {
        let n: u16 = s.parse()?;  // auto-boxed
        if n == 0 { return Err("port cannot be zero".into()); }
        Ok(n)
    }
    

    OCaml

    (* Use exceptions for untyped errors *)
    exception AppError of string
    
    let parse_port s =
      match int_of_string_opt s with
      | None -> raise (AppError "invalid port")
      | Some n when n = 0 -> raise (AppError "port cannot be zero")
      | Some n -> n
    

    Pattern: Adding Context

    Rust

    let port = parse_port(port_str).context("invalid port")?;
    

    OCaml

    try parse_port s with
    | AppError msg -> raise (AppError ("invalid port: " ^ msg))
    

    Key Differences

    ConceptOCamlRust
    Untyped errorexn (exceptions)Box<dyn Error>
    String as errorFailure "msg""msg".into()
    ContextCatch and re-raise.context() extension
    Library vs appSame mechanismLibrary: typed; App: boxed
    Thread safetyN/A+ Send + Sync bounds

    Exercises

  • Implement a context(msg) extension method on Result<T, E> that wraps any error in a WithContext struct.
  • Write a function that calls five different operations each returning different error types, using AnyResult<T> to unify them without any map_err.
  • Traverse the full error chain of a nested WithContext error and display each level with its message.
  • Open Source Repos