Alright, let’s talk about the source() method. This is the secret handshake of the Rust error ecosystem, the mechanism that lets one error politely point to the cause of another, forming a chain of blame that you can follow all the way back to the root cause. It’s how we get those beautiful, multi-line error reports that actually tell you what went wrong instead of just coyly shrugging.

Think of it like this: you ask your friend why the party was cancelled. They say, “The venue flooded.” That’s the error. The source() method is you asking, “Why was it flooded?”, and your friend replying, “A pipe burst.” That’s the source. You can keep asking “why?” until you get to the root cause, which is probably some contractor in the 80s using the wrong kind of pipe. The std::error::Error trait’s source() method formalizes this “why?” question.

The Nuts and Bolts of source()

The source() method is defined in the standard Error trait. It returns an Option<&(dyn Error + 'static)>—essentially, a possible reference to another error trait object. The 'static lifetime bound is crucial here; it means the returned error must be something that lives as long as the program, which almost always means it’s borrowed from self. You’re not taking ownership, you’re just pointing to a part of yourself.

Let’s build a classic error chain from scratch to see how this fits together. Imagine a function that reads a config file and parses it.

use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io;

// A custom error for our config parsing operation
#[derive(Debug)]
struct ParseError {
    message: String,
    source: Option<Box<dyn Error + 'static>>, // The source is stored here
}

// This is how we implement the `Error` trait manually.
impl Error for ParseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        // Delegate to the inner field. We need to map the `Box` to a reference.
        self.source.as_ref().map(|e| e.as_ref())
    }
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Failed to parse config: {}", self.message)
    }
}

// A function that could fail in different ways
fn read_config(path: &str) -> Result<String, ParseError> {
    let mut file = File::open(path).map_err(|e| ParseError {
        message: "Could not open file".to_string(),
        source: Some(Box::new(e)), // Chain the io::Error as the source
    })?;

    // ... let's pretend we try to read and parse the file contents here ...
    // and if that parsing logic fails, we might do:
    Err(ParseError {
        message: "Invalid TOML syntax".to_string(),
        source: Some(Box::new(io::Error::new(io::ErrorKind::InvalidData, "Not valid TOML"))),
    })
}

The key is in the map_err: when the File::open call fails, we catch the io::Error and box it up, storing it inside our own, more contextual ParseError. We’ve just created a link in the chain.

How thiserror Makes This Effortless

Doing this manually is a chore. You have to define a struct, implement Display, implement Error, and wire up the source field correctly. It’s boilerplate, and boilerplate is an error-prone waste of our time. This is exactly why thiserror exists.

The #[from] attribute is the star of the show here. It does all the manual work we did above automatically.

use thiserror::Error;

#[derive(Error, Debug)]
enum MyConfigError {
    #[error("Could not open file at {path:?}")]
    FileOpen {
        path: String,
        #[source] // This attribute is key!
        source: io::Error,
    },
    #[error("Failed to parse config as TOML")]
    ParseFailure(#[from] toml::de::Error), // Even simpler with `#[from]`
}

See what happened there? The #[source] attribute on the source field in the variant tells thiserror to use that field for the source() method’s return value. The #[from] attribute on the ParseFailure variant is even more powerful: it automatically generates a From<toml::de::Error> conversion, so you can use ? directly, and it implicitly sets that converted error as the source. It’s two birds with one stone, and it’s fantastic.

The anyhow Shortcut: context()

Now, what if you’re in an application, not a library, and you don’t care about bespoke error types? You just want to add context and get a chain. This is anyhow’s playground. While thiserror is for defining error types, anyhow is for handling and propagating them with ease.

Its secret weapon is the Context trait, which adds context() and with_context() methods to Result.

use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)
        .context(format!("Failed to read config from {}", path))?; // Chain added here!

    // Parse the content...
    Ok(content)
}

fn main() -> Result<()> {
    let _config = read_config("bad_file.toml")?;
    Ok(())
}

If read_to_string fails with an io::Error, the context() method wraps it inside an anyhow::Error, embedding your message as the new “layer” of the error. The original io::Error becomes the source. When you print this error with the {:#} formatter (eprintln!("Error: {:#}", error)), anyhow will walk the entire source chain and display every context message in order, giving you a perfect, human-readable story of what went wrong and why.

Common Pitfalls and Sharp Edges

  1. The 'static Bound: Remember, source() returns a &dyn Error + 'static. This means you cannot return a reference to an error that holds non-'static data (like a &str). This is why we almost always Box the source error—the box itself is 'static, even if the data inside it has a shorter lifetime, as long as we’ve moved ownership into the box.
  2. Cyclic Chains: It’s possible, though usually daft, to create a cycle where error A’s source is error B, and error B’s source is error A. The standard library’s Error trait’s provided method for iterating the chain (sources()) uses &self, so it’s safe, but it would just loop forever. Don’t do this.
  3. Over-Chaining: There’s such a thing as too much of a good thing. Wrapping every single error in five layers of “failed to do X” context just creates noise. Add context where it’s truly helpful for diagnosing the problem. “Failed to save user preferences” is good context. “Called the save function” is not.