When your type isn't an error yet
You're writing a function that returns Result<T, E>. You try to use ? to propagate an error, or you pass your custom error to a function that expects impl Error. The compiler stops you with a wall of text about trait bounds. Your type is missing something the compiler considers essential for being an error.
The error message usually points to std::error::Error. It says the trait bound is not satisfied. This isn't a bug in your logic. It's a contract violation. You promised a type that behaves like an error, but you delivered a struct with no error capabilities. The compiler refuses to mix your type with generic error handlers until you prove it follows the rules.
The Error trait is a protocol
Rust's type system relies on traits to define behavior. The std::error::Error trait is the standard interface for error types. It guarantees three things. The type can be printed for the user. The type can be debugged by a developer. The type can be inspected for a cause. This uniformity allows generic code to handle any error the same way.
Think of Error like a universal remote control protocol. You can build a custom TV, but if it doesn't speak the IR protocol, the remote can't turn it on. The Error trait is the protocol. Implementing it tells the compiler your type speaks the language of errors. Without it, your type is just data. With it, your type becomes a first-class error that integrates with Result, ?, and logging tools.
Minimal fix
The fix is straightforward. Implement Error for your type. The trait has supertraits, so you must also implement Debug and Display. Debug is for developers. Display is for users.
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ParseError;
// Error requires Display. This provides the user-facing message.
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse input")
}
}
// This impl satisfies the Error trait bound.
impl Error for ParseError {}
fn main() {
let err = ParseError;
// This compiles. ParseError implements Error.
report_error(err);
}
fn report_error<E: Error>(e: E) {
println!("Error: {}", e);
}
The #[derive(Debug)] macro handles the developer view. The Display impl handles the user view. The empty impl Error for ParseError {} ties them together. Now ParseError satisfies any bound requiring Error.
What the compiler checks
When the compiler sees a function signature like fn foo<E: Error>(e: E), it builds a checklist. It looks at the type you pass in. It asks: "Does this type implement Error?" If the answer is no, compilation halts. The error code is E0277. The message points to the missing trait.
If you implement Error but forget Display, you still get E0277. The compiler rejects this with E0277 (the trait bound MyType: std::fmt::Display is not satisfied). The error might mention Display instead of Error, which can be confusing. The root cause is the same. Error requires Display. You can't have one without the other.
At runtime, nothing changes. Traits are erased or monomorphized. The check is purely a compile-time contract enforcement. Once the code compiles, the trait bounds are gone. The binary just runs.
Realistic code uses macros
In real projects, you rarely write impl Error by hand. You use macros. thiserror is the community standard for library errors. It generates the Error and Display impls automatically. It also handles conversions via #[from].
// Real projects use thiserror to reduce boilerplate.
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("IO failure: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid config: {0}")]
Config(String),
}
fn load_config() -> Result<String, AppError> {
// The ? operator works because AppError implements From<io::Error>.
let data = std::fs::read_to_string("config.json")?;
Ok(data)
}
The #[derive(Error)] macro generates the impl Error block. The #[error(...)] attribute generates Display. The #[from] attribute generates impl From<io::Error>, which allows ? to convert io::Error into AppError::Io.
Convention aside: The community splits error handling into two camps. Libraries define custom error types using thiserror so callers can match on variants. Applications use anyhow::Result to avoid defining enums for every little thing. Pick thiserror when you're writing a crate others will depend on. Pick anyhow for the main function or internal scripts.
Error chains and source
The Error trait supports chains. The source() method returns the underlying cause. This allows wrapping errors while preserving context. When you wrap a low-level error in a high-level error, implement source() to link them. Debuggers and logging tools use this to print the full stack.
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct NetworkError {
inner: std::io::Error,
}
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "connection timed out")
}
}
impl Error for NetworkError {
// Link to the root cause.
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.inner)
}
}
The source() method returns Option<&(dyn Error + 'static)>. If the error has a cause, return Some(&cause). If it's a root error, return None. This chain lets you report "connection timed out" while preserving the original "timeout" from the OS.
Always implement source() when wrapping another error. It saves hours of debugging later.
Pitfalls
The most common trap is forgetting Display. Error requires Display. If you derive Debug but skip Display, you get E0277 for Display. The compiler rejects this with E0277 (the trait bound MyType: std::fmt::Display is not satisfied).
Another trap is using Box<dyn Error> without importing. If you write Box<dyn Error> but don't have use std::error::Error;, the compiler rejects this with E0412 (cannot find type Error in this scope). The fix is to import the trait or use the full path Box<dyn std::error::Error>.
A third trap is assuming Error is enough for ?. The ? operator requires From. If you return Result<T, AppError> and try to use ? on io::Error, you need impl From<io::Error> for AppError. thiserror handles this with #[from]. Without it, you get a different error about From not being implemented.
Check the supertraits. Error demands Debug and Display. Miss one, and the impl fails silently until you hit the bound check.
When to use what
Use impl Error manually for learning or tiny scripts where adding dependencies is overkill.
Use thiserror for library crates where you define error enums and need automatic conversions.
Use anyhow for application binaries where you want to propagate errors without defining types.
Use Box<dyn Error> when returning multiple unrelated error types and you cannot define an enum.
Don't reach for Box<dyn Error> just because it's convenient. It allocates on the heap and erases type information. Define an enum if the error space is known.