Good error types make failures diagnosable; the thiserror/anyhow split is the ecosystem's standard pattern.
A proper error type implements `std::error::Error`, which requires Debug and Display, and optionally provides source() to expose the underlying cause. Callers can then print it, box it as Box<dyn Error + Send + Sync>, and walk the chain of causes.
For a library, define an enum of the failures callers might want to react to:
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("cannot read {path}")]
Io { path: String, #[source] source: std::io::Error },
#[error("invalid port {0}")]
BadPort(u32),
#[error(transparent)]
Parse(#[from] std::num::ParseIntError),
}The thiserror derive writes the Display, Error::source and From impls you would otherwise write by hand; it adds nothing to your public API. Keep variants meaningful: callers match on them.
For an application, you usually just want to report what went wrong with enough context. anyhow provides anyhow::Result<T> (with an anyhow::Error that wraps any error), .context("loading config") and .with_context(|| format!(...)) on both Result and Option, plus bail! and ensure! macros. Printing with {:#} shows the whole cause chain on one line, and {:?} adds a backtrace when RUST_BACKTRACE=1.
Guidelines that hold up:
- Do not put the source error's message into your own
Displayand also return it fromsource(), or reporters print it twice. - Make library errors
Send + Sync + 'staticso they work across threads and async tasks. - Mark public error enums
#[non_exhaustive]if you may add variants later. - Libraries should not depend on anyhow in their public API; applications can freely mix both.
The example below implements the same pattern without crates, so you can see what the derive generates.
Example
use std::{error::Error, fmt, num::ParseIntError};
#[derive(Debug)] enum ConfigError { MissingKey(&'static str), BadPort(ParseIntError) }
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::MissingKey(k) => write!(f, "missing key `{k}`"),
Self::BadPort(_) => write!(f, "port is not a number"), // the cause comes via source()
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { Self::BadPort(e) => Some(e), _ => None }
}
}
impl From<ParseIntError> for ConfigError { // what thiserror's #[from] generates
fn from(e: ParseIntError) -> Self { Self::BadPort(e) }
}
fn port(line: &str) -> Result<u16, ConfigError> {
Ok(line.strip_prefix("port=").ok_or(ConfigError::MissingKey("port"))?.parse()?)
}
fn main() {
let err = port("port=http").unwrap_err();
println!("{err}; caused by: {}", err.source().unwrap()); // port is not a number; caused by: invalid digit found in string
}Remember
- Error types implement
Debug + Display + Error, exposing causes viasource() - Libraries: specific enums, often derived with thiserror
- Applications: anyhow with
.context()for readable chains
Watch out for
- Returning
Result<T, String>from libraries, which callers cannot match on or chain.