When the default error message is a wall of nothing
You wrote a Rust program. Something went wrong. You got back something like this:
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
That's not nothing. But it's also not enough. Where did the error happen? What was the program trying to do? If you returned ? from three different functions, which one fired? You're left guessing, sprinkling dbg! around, and hoping you guessed right.
The color-eyre crate exists to make this moment less painful. It takes the standard "boxed error" pattern and dresses it up: colored output so the eye finds the important parts, a backtrace pointing at the exact source line, and section headers that turn a confusing error into a debug report you can actually read. It's the kind of crate you install once and then forget you installed, because suddenly your error messages are doing the work for you.
What problem is it actually solving
Rust's error story has two halves. There's the type system (the Result<T, E> and ? operator stuff) and there's the formatting story (what shows up on stderr when something fails).
The type half is well-loved. The formatting half is bare-bones by default. Result<T, Box<dyn Error>> returned from main will print something like Error: <Display of the inner error> and exit. No backtrace. No source chain. No color. Fine for a one-off CLI utility, agonizing the moment your program does anything interesting.
color-eyre plugs into a crate called eyre, which is itself a fork of the popular anyhow crate. The split: anyhow is the one most people use; eyre lets you customize how errors are reported. color-eyre is the most popular custom reporter. It uses the color-spantrace crate (for tracing context), the color-backtrace crate (for prettier stack traces), and ANSI colors. The result looks like it was designed by someone who had to debug Rust at 2am and got tired of squinting.
The minimal setup
Two pieces. Add the crate to Cargo.toml, then call color_eyre::install() once at the top of main. That's it.
# Cargo.toml
[dependencies]
# eyre gives us the Result type; color-eyre gives us the pretty handler
color-eyre = "0.6"
use color_eyre::eyre::Result; // Result<T> aliased to Result<T, eyre::Report>
fn main() -> Result<()> {
// install() must run before any error is constructed, so put it first.
// It returns Result, so propagate with ?.
color_eyre::install()?;
// Any error returned from here gets the colored treatment automatically.
let contents = std::fs::read_to_string("does-not-exist.txt")?;
println!("{contents}");
Ok(())
}
Run that and you get something like:
Error:
0: No such file or directory (os error 2)
Location:
src/main.rs:8
Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Already an upgrade. You can see exactly which line returned the error. Set RUST_BACKTRACE=1 and you also get the full call stack.
Adding context with wrap_err
The colored formatting is nice, but the real superpower is wrap_err. It lets you attach a human-readable label to any error as it bubbles up. Each label becomes a layer in the printed report.
use color_eyre::eyre::{Result, WrapErr}; // bring .wrap_err() into scope
// Read the user's config from a known path. Wraps the IO error with a hint.
fn load_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
// wrap_err_with takes a closure so the format! only runs on the error path
.wrap_err_with(|| format!("failed to read config at {path}"))
}
// Parse the config text into JSON. Wraps the parse error with a constant string.
fn parse_config(text: &str) -> Result<serde_json::Value> {
serde_json::from_str(text)
.wrap_err("config file is not valid JSON")
}
fn main() -> Result<()> {
color_eyre::install()?;
// Each ? bubbles a wrapped error up. The chain is preserved in order.
let text = load_config("/etc/myapp/config.json")?;
let _cfg = parse_config(&text)?;
Ok(())
}
If the file is missing, the report tells you what was happening at every level:
Error:
0: failed to read config at /etc/myapp/config.json
1: No such file or directory (os error 2)
Location:
src/main.rs:5
That numbered list is the error chain. The top is what your code was doing. The bottom is the underlying cause. You don't have to format it yourself; wrap_err does the threading and color-eyre prints it.
How it interacts with tracing
If you also use the tracing crate (very common in async services), color-eyre will pick up the active span tree and print it alongside the error. That means you see the request ID, user ID, or whatever else you put in your spans, without explicitly threading them through every function.
use color_eyre::eyre::{eyre, Result};
use tracing::instrument;
// #[instrument] auto-generates a span named after the function and records args.
#[instrument]
fn handle_request(user_id: u64) -> Result<()> {
do_work()?; // any error in here gets the span attached
Ok(())
}
// A made-up failure to demonstrate the span trace.
fn do_work() -> Result<()> {
Err(eyre!("the database said no"))
}
fn main() -> Result<()> {
color_eyre::install()?;
tracing_subscriber::fmt::init();
handle_request(42)
}
The error report will include handle_request{user_id=42} in a "SpanTrace" section. That's the kind of detail that turns a bug report from "something failed" into "something failed while serving user 42." Great for production logs, even better for local debugging when you're trying to reproduce an issue.
Common gotchas
A few things that trip people up.
The install() call has to come first, before any error is constructed. If you build a Report and then call install(), the early reports won't have the pretty handler attached. The fix is to put install()? as the first statement of main.
Backtraces are off by default. You have to opt in with RUST_BACKTRACE=1 or the more detailed RUST_BACKTRACE=full. In a development shell you can export RUST_BACKTRACE=1 once and forget about it. In CI, set it as an environment variable for the test job; you'll thank yourself the next time a test fails on a remote machine.
color-eyre uses ANSI escape codes. If you pipe stderr to a file or to a tool that doesn't strip them, you get gibberish like ^[[31m. The crate detects whether stderr is a tty and skips colors if not, but some pipe setups confuse the detection. If you need to force colors off, set NO_COLOR=1. If you need to force them on (e.g. for a CI log viewer that handles ANSI), set CLICOLOR_FORCE=1.
Don't pair it with anyhow. They use the same underlying machinery, and the macros (anyhow! vs eyre!) are similar but not interchangeable. Pick one per crate and stick to it. If you're starting fresh and want pretty errors, use color-eyre. If you don't care about formatting and just want the boxed-error pattern, anyhow is fine.
When this is the right tool
color-eyre is for application code: binaries, CLI tools, services. It's not for libraries. A library should expose its own error type (often a thiserror-derived enum) so callers can match on it and decide how to react. Wrapping every library error in an opaque eyre::Report would force callers to do string matching on the error message, which is gross.
In a binary crate, on the other hand, nobody calls into your code. You just want errors to bubble up to main and print something useful before exiting. That's exactly the niche color-eyre fills. It's especially nice for prototypes, internal tools, and services where you control the runtime and want the best possible diagnostics for the smallest amount of setup.
If you find yourself wanting both, mix them: derive thiserror enums for your library code, then wrap them in eyre::Report at the application boundary with wrap_err. The library has a clean typed API; the binary has a great error report.