Right, so you’ve got your error all boxed up in an anyhow::Error. It’s wonderfully convenient, but now you need to actually do something with it. You can’t just shrug and say “something went wrong” to the user; you need to tell them their database connection string was malformed, or that the configuration file is missing a required section. This is where we go from error handling to error interrogation. We need to crack that anyhow::Error open to see what’s inside. The process is called downcasting.

Think of an anyhow::Error as a black box that implements the standard std::error::Error trait. Inside, it’s holding onto the specific error type you gave it, erasing its concrete type. Downcasting is our way of asking, “Hey, I know you’re hiding something in there. Are you, by any chance, a std::io::Error? Or maybe a MyCustomError?” We’re trying to reverse the type erasure.

The Basic downcast Method

The most straightforward way is to use anyhow::Error::downcast. You call it, specifying the type you’re looking for. If you guess correctly, you get back an owned Ok(T). If you’re wrong, you get back the original Err(anyhow::Error) so you can try downcasting to something else. It’s like a polite, typed version of “guess who?”

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

#[derive(Debug)]
struct ConfigParseError {
    line: usize,
    message: String,
}

fn read_config() -> Result<String> {
    // Let's simulate an error: the file doesn't exist, which is an std::io::Error.
    fs::read_to_string("config.toml").context("Failed to read config file")
}

fn handle_error(error: anyhow::Error) {
    // Try downcasting to an io::Error first.
    if let Err(e) = error.downcast::<std::io::Error>() {
        // That wasn't it! `e` is still an `anyhow::Error`. Let's try our custom error next.
        if let Err(e) = e.downcast::<ConfigParseError>() {
            // Still not it? Fine, give up and use the generic display.
            eprintln!("An unexpected error occurred: {}", e);
        } else {
            eprintln!("We successfully downcast to ConfigParseError!");
        }
    } else {
        eprintln!("We successfully downcast to std::io::Error!");
    }
}

fn main() {
    let result = read_config();
    if let Err(e) = result {
        handle_error(e);
    }
}
// This will print: "We successfully downcast to std::io::Error!"

This works, but it’s a bit clunky. You’re left manually unpacking the error like a Russian nesting doll, and you end up with a confusing double-negative pattern (if let Err(e) = error.downcast() meaning “if it IS this type”…). It’s effective, but not exactly elegant.

The downcast_ref Shortcut

More often, you don’t want to consume the error and take ownership of the inner type. You just want to peek inside, check its type, and perhaps log it or make a decision based on it. For this, use downcast_ref. It gives you a borrowed reference, which is what you want 99% of the time.

fn handle_error_smarter(error: &anyhow::Error) {
    if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
        eprintln!("It's an IO error! Kind: {:?}", io_error.kind());
        // We can now interact with the full io::Error API.
    } else if let Some(parse_error) = error.downcast_ref::<ConfigParseError>() {
        eprintln!("Config error on line {}: {}", parse_error.line, parse_error.message);
    } else {
        // We never consumed `error`, so we can still use it here.
        eprintln!("Generic error: {}", error);
    }
}

This is the pattern you’ll use most frequently. It’s clean, efficient, and lets you inspect the error without destroying it.

The Chain of Responsibility

Here’s where anyhow gets its real power. Remember those context() calls you’ve been adding? They create a chain of errors. When you downcast, anyhow doesn’t just look at the root cause; it walks the entire chain of errors from the most specific cause back to the most general context. It will find your target type anywhere in that chain.

fn read_config() -> Result<String> {
    let content = fs::read_to_string("config.toml")
        .context("Couldn't find or read config.toml")?; // Adds context to an io::Error

    // Let's say parsing fails with our custom error...
    if content.is_empty() {
        return Err(anyhow!(ConfigParseError { line: 1, message: "File is empty".to_string() }))
        .context("Config file was found but invalid"); // Adds context to a ConfigParseError!
    }
    Ok(content)
}

fn handle_error_chain(error: &anyhow::Error) {
    // This will succeed! Even though the error is wrapped in multiple layers of context,
    // downcast_ref will find the ConfigParseError inside the chain.
    if let Some(parse_error) = error.downcast_ref::<ConfigParseError>() {
        eprintln!("Found the root parse error deep down: {:?}", parse_error);
    }

    // This will also succeed, because the io::Error is also still in the chain.
    if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
        eprintln!("Found the root io error too: {:?}", io_error);
    }
}

This is brilliantly useful. It means you can add all the contextual information you want for debugging and user messages, while still being able to reliably pinpoint the root cause for programmatic handling. You’re not forced to choose between good error messages and actionable error types.

The is Method for a Quick Check

Sometimes you don’t care about the details of the error, you just need to know if it’s of a certain type to decide on a course of action. For this, use the is method. It’s a boolean check that tells you if downcasting would succeed.

if error.is::<std::io::Error>() {
    eprintln!("Yep, it's an IO error. Let's retry the operation.");
} else if error.is::<ConfigParseError>() {
    eprintln!("It's a parse error. We need to tell the user to fix their config.");
}

It’s a lightweight way to make decisions without the overhead of actually downcasting.

The One Major Pitfall: Downcasting to anyhow::Error

This is the classic rookie mistake, and the compiler won’t save you from it. anyhow::Error itself implements the Error trait. So if you try to downcast to anyhow::Error, it will always succeed. You’ll just get a reference to itself. This is completely useless.

// This is ALWAYS true. It's a tautology. Don't do this.
if let Some(useless) = my_error.downcast_ref::<anyhow::Error>() {
    // `useless` is just another reference to `my_error`
}

You downcast to the concrete type inside the anyhow::Error (like io::Error, MyCustomError), not to the anyhow wrapper itself. This is the most common “head-scratcher” moment people have with anyhow, so consider yourself warned. You’re now smarter than past-you.