When errors need context
You are writing a function that reads a configuration file, parses it, and connects to a database. Halfway through, the file is missing. You want main to print "Failed to load config: file not found", but you also want to avoid writing a 50-line error enum just to glue three different error types together. You reach for ?, but the compiler complains about mismatched types. You need a way to wrap errors with context and propagate them without the boilerplate.
That is where eyre steps in. Reach for eyre when you need context without the enum boilerplate.
The bubble wrap analogy
eyre gives you a single error type that can hold any error. It works by wrapping errors in layers of context. Imagine you are passing a fragile package up a chain of supervisors. Each supervisor adds a sticky note describing what they were doing when the package arrived damaged. By the time the package reaches the CEO, you have a full trail of notes explaining exactly where and how things broke.
eyre does this for Rust errors. It lets you use ? everywhere because every error gets converted into the same eyre::Report type. You get rich context without defining custom enums. Treat the error as a package. Wrap it, don't unwrap it.
Minimal example
use eyre::{Result, WrapErr};
/// Attempts to parse a number from a string.
/// Returns the number or an eyre Report if parsing fails.
fn parse_number(input: &str) -> Result<i32> {
// The ? operator propagates the ParseIntError.
// eyre automatically converts it to a Report via From trait.
let num = input.parse::<i32>()?;
Ok(num)
}
fn main() -> Result<()> {
// Wrap the call with context.
// If parse_number fails, this context is attached to the error chain.
let num = parse_number("abc")
.wrap_err("Failed to parse configuration value")?;
println!("Number is {}", num);
Ok(())
}
How eyre propagates errors
When you compile, eyre::Result<T> expands to Result<T, eyre::Report>. The ? operator relies on the From trait. eyre implements From<E> for almost any type E that implements std::error::Error. This means you can return ? from a function returning eyre::Result regardless of the specific error type.
At runtime, eyre boxes the error. The Report type holds a pointer to the error data and a chain of context strings. When you print the report, it walks the chain and displays the context layers from top to bottom, ending with the root cause. Trust the ? operator. It converts and propagates automatically.
Realistic example: Config loader
use eyre::{bail, Result, WrapErr};
use std::fs;
/// Loads configuration from a file path.
/// Returns the content as a string or an eyre Report.
fn load_config(path: &str) -> Result<String> {
// Read the file. fs::read_to_string returns io::Result.
// The ? converts io::Error to eyre::Report.
// wrap_err adds a layer of context describing the operation.
let content = fs::read_to_string(path)
.wrap_err(format!("Failed to read config file '{}'", path))?;
// Validate content.
if content.is_empty() {
// bail! creates a Report from a format string.
// This is equivalent to return Err(eyre!(...)).
bail!("Config file '{}' is empty", path);
}
Ok(content)
}
fn main() -> Result<()> {
// Call the function and handle the result.
match load_config("config.toml") {
Ok(content) => println!("Loaded {} bytes", content.len()),
Err(e) => {
// Print the full error chain.
// eyre formats this nicely with context.
// Use Debug formatting {:?} to see the chain.
eprintln!("Error: {e:?}");
}
}
Ok(())
}
Context chains and debugging
The eyre::Report type stores a linked list of context layers. Each call to wrap_err pushes a new layer onto the chain. The root error sits at the bottom. When you format the report, eyre traverses this chain to produce output.
There is a crucial difference between Display and Debug formatting. The Display implementation only shows the topmost context layer. The Debug implementation shows the entire chain, including the root error and all intermediate context. This distinction matters for logging.
use eyre::Result;
fn main() -> Result<()> {
let report = Err::<(), _>("root error".into())
.wrap_err("middle context")
.wrap_err("top context")
.unwrap_err();
// Display shows only the top layer.
println!("Display: {}", report);
// Output: Display: top context
// Debug shows the full chain.
println!("Debug: {report:?}");
// Output: Debug: top context
// --> middle context
// --> root error
Ok(())
}
Use Debug for logs. Use Display for user messages. The community convention is to print errors with eprintln!("Error: {e:?}") in application binaries. This ensures developers see the full context when debugging.
Pitfalls and compiler errors
eyre is designed for applications, not libraries. If you expose eyre::Result in a public API, users cannot match on the error type. They only get a black box. They cannot distinguish between a file not found error and a database timeout. This breaks the contract of a library.
Another pitfall involves backtraces. eyre can capture backtraces, but they are disabled by default to save memory and initialization time. You need to set the environment variable RUST_BACKTRACE=1 to enable them. Even then, backtraces only appear if the backtrace crate is available and the error type supports it. Most standard errors do.
Compiler errors appear when you mix types incorrectly. If you try to return a std::io::Error from a function returning eyre::Result without using ?, the compiler rejects it with E0308 (mismatched types). The ? operator is required to trigger the conversion.
If you try to wrap a type that does not implement std::error::Error, you get E0277 (trait bound not satisfied). eyre requires the error to implement the standard trait. Primitive types like i32 or bool do not implement Error. You must convert them to strings or custom error types first.
Keep eyre in the binary. Expose enums from libraries.
Decision: eyre versus alternatives
Use eyre when you are writing an application binary and want to propagate errors with context without defining custom types. Use eyre when you need a unified error type that accepts any std::error::Error and supports ? propagation. Use eyre when you want detailed error reports with context chains and optional backtraces for debugging.
Use custom error enums when you are building a library and need to expose precise error types so callers can match on specific failures. Use custom error enums when the error domain is small and well-defined, like a parser with distinct syntax errors.
Use thiserror when you need to define custom error enums with less boilerplate than manual trait implementations. Use thiserror in libraries where you still want to expose specific error types but want the derive macro to handle the std::error::Error implementation.
Use anyhow when you prefer a crate with a slightly different API surface or formatting style, though eyre is generally the modern recommendation for new projects.
Libraries expose types. Applications handle reports.