The Rust book says Rust has no exceptions. Recoverable errors use Result<T, E>. You choose what E is.
Plain enums
The Error trait only requires that Debug and Display are implemented too. That costs a dependency-free but wordy Display impl.
#[derive(Debug)]
enum ConfigError { Missing(String) }
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { Self::Missing(k) => write!(f, "missing key {k}") }
}
}
impl std::error::Error for ConfigError {}
thiserror for libraries
thiserror derives Error and Display from attributes. Its docs say it “deliberately does not appear in your public API”, so switching to or from it is not a breaking change. #[from] generates From conversions.
#[derive(Debug, thiserror::Error)]
enum ConfigError {
#[error("missing key {0}")]
Missing(String),
#[error("cannot read file")]
Read(#[from] std::io::Error),
}
anyhow for applications
anyhow is a trait-object error type for applications. It accepts any std::error::Error through ? and lets you attach context. You give up a named type to match on. The docs describe downcasting for when you need one.
use anyhow::{Context, Result};
fn main() -> Result<()> {
let text = std::fs::read_to_string("tool.toml").context("reading tool.toml")?;
println!("{} bytes", text.len());
Ok(())
}
Which to pick
A one-file tool with one or two failures can use a plain enum. A library other code will match on should use thiserror. The binary on top should use anyhow.

The Campfire
No commentsNobody has pulled up a log by this one yet. Be the first to say what you make of it.
Held for the desk. It appears after a look.