The Error traitConcept
Explanation
std::error::Error (now core::error::Error, stabilized there since
Rust 1.81) is the standard interface an error type is expected to
implement. Its requirements are small: a type must already implement
Display (a human-readable message) and Debug (a developer-facing
representation), and the trait itself adds exactly one method with a
default implementation — source(), returning
Option<&(dyn Error + 'static)>, the underlying cause this error wrapped,
if any. That's the entire contract; the trait carries no data of its
own.
It exists so that generic, error-agnostic code can exist at all. Without
a shared trait, a top-level main, a logging layer, or an application's
error-reporting boundary would need to know the concrete error type of
every fallible call it might ever see — completely impractical once a
program depends on more than a handful of crates. Implementing Error
is what lets an error be boxed into Box<dyn Error>, logged uniformly,
and chased back through source() to its root cause, regardless of
which crate defined it.
The mental model is a chain, not a single value: many failures are really
"this failed, because that failed, because the thing under that
failed." source() is how a custom error type exposes the next link
down without collapsing the whole chain into one string up front — a
caller (or a reporting tool) can walk source() repeatedly until it
returns None, printing or inspecting each cause in turn.
Custom error types covers designing an error
enum's variants and data; this page is about the trait those types
implement to interoperate with the rest of the ecosystem. thiserror's
derive generates an Error impl (and often From conversions) from
attributes, but it's still worth seeing the manual version at least
once — the examples below implement Error by hand, including a manual
source() and a manual From impl, so the derive's shorthand has a
concrete mechanism behind it rather than being magic.
A function boundary that doesn't want to commit to one concrete error
type at all can return Result<T, Box<dyn Error>> (or
Box<dyn Error + Send + Sync> when the box must cross threads), since
any type implementing Error can be boxed into it, and
? converts into that box
automatically — the same mechanism anyhow::Error builds a more
ergonomic wrapper around for application code.
Basic usage example
use std::fmt;
#[derive(Debug)]
struct EmptyQueueError;
impl fmt::Display for EmptyQueueError { // <- Error requires Display for a human-readable message
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "cannot dequeue: the queue is empty")
}
}
impl std::error::Error for EmptyQueueError {} // <- the trait itself: no methods required beyond its supertraits here
Best practices & deeper information
Handling and propagating errors
A database layer wraps a lower-level I/O failure; implementing Error
manually (no thiserror) and overriding source() keeps the original
cause discoverable instead of losing it inside a formatted string.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
struct QueryError {
query: String,
cause: std::io::Error,
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "query failed: {}", self.query)
}
}
impl Error for QueryError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.cause) // <- exposes the underlying io::Error as the chained cause
}
}
fn log_chain(err: &dyn Error) {
eprintln!("{err}");
let mut cause = err.source();
while let Some(inner) = cause {
eprintln!("caused by: {inner}");
cause = inner.source();
}
}
Why this way: overriding source() instead of folding the cause's
message into Display keeps the two concerns separate — the top-level
message stays short, and tools (or the manual chain-walk shown here) can
still reach the full cause chain, which is the purpose the
std::error::Error docs
give for the method.
Designing a public API
A function that can fail for many unrelated reasons — parsing, I/O, a third-party crate's own error type — doesn't want to invent one enum covering all of them, so it returns a boxed trait object instead.
use std::error::Error;
fn load_and_parse(path: &str) -> Result<u32, Box<dyn Error>> {
// <- `Box<dyn Error>`: accepts any concrete error type behind one boundary
let contents = std::fs::read_to_string(path)?; // io::Error boxes automatically
let value: u32 = contents.trim().parse()?; // ParseIntError boxes automatically too
Ok(value)
}
Why this way: Box<dyn Error> is the standard escape hatch for an
application-level boundary that would rather not define a bespoke enum
for every combination of underlying failures — every concrete error type
? encounters converts into the box automatically, because
Box<dyn Error> implements From<E> for any E: Error, which is exactly
why anyhow::Error (an ergonomic wrapper over the same idea) is blessed
for application code under this scenario's crate policy.
Converting between types
Pairing a manual Error impl with a manual From impl is what lets ?
convert an underlying error into a custom type without a derive macro
doing it implicitly.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum CacheError {
Backend(std::io::Error),
}
impl fmt::Display for CacheError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CacheError::Backend(e) => write!(f, "cache backend failed: {e}"),
}
}
}
impl Error for CacheError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
CacheError::Backend(e) => Some(e),
}
}
}
impl From<std::io::Error> for CacheError { // <- written by hand, no #[from] derive involved
fn from(e: std::io::Error) -> Self {
CacheError::Backend(e)
}
}
fn read_cache_entry(path: &str) -> Result<String, CacheError> {
Ok(std::fs::read_to_string(path)?) // <- `?` uses the hand-written `From` impl above
}
Why this way: writing From by hand alongside a manual Error impl
shows exactly what thiserror's #[from] attribute (see
custom error types) generates — understanding
the manual mechanism makes the derive's shorthand legible rather than
magic, the same order the
Rust Book
takes by showing manual error handling before reaching for crates that
automate it.