Custom error types in Rust: enums, Display and From
How to write your own error type in Rust: an error enum, impl Display and std::error::Error, From conversions for ?, and the thiserror shortcut.
Result<T, String> works for small programs, but callers can't do much with a string except print it. A custom error type lets them react to specific failures (retry on a timeout, show a form error for bad input) and keeps the original cause around.
Step 1: an enum of everything that can go wrong
#[derive(Debug)]
enum ConfigError {
Missing(String), // which key was missing
BadNumber { key: String, source: std::num::ParseIntError },
OutOfRange(u32),
}
Each variant carries the details needed to explain or handle it.
Step 2: implement Display and Error
Display is the message for humans; std::error::Error marks it as a proper error type, so it works with Box<dyn Error>, anyhow, and error-reporting tools. source() returns the underlying cause, if any:
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Missing(key) => write!(f, "missing key `{key}`"),
ConfigError::BadNumber { key, .. } => write!(f, "`{key}` is not a number"),
ConfigError::OutOfRange(n) => write!(f, "{n} is out of range"),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::BadNumber { source, .. } => Some(source),
_ => None,
}
}
}
Convention: messages are lowercase with no trailing period, and don't repeat the source's message (reporters print the chain).
Step 3: From, so ? converts for you
The ? operator calls From::from on the error. Implement From<OtherError> for ConfigError and ? turns that error into yours automatically. Without it:
#[derive(Debug)]
enum ConfigError {
Missing,
}
fn port(s: &str) -> Result<u16, ConfigError> {
let p = s.parse::<u16>()?;
Ok(p)
}
fn main() {
println!("{:?} {:?}", port("80"), ConfigError::Missing);
}
error[E0277]: `?` couldn't convert the error to `ConfigError`
When a variant needs extra context (like which key failed), use map_err at the call site instead of a blanket From.
Box: the quick alternative
For scripts and main, Result<T, Box<dyn std::error::Error>> accepts any error type through ? with no boilerplate. The cost: callers can't easily match on specific errors.
thiserror: the same thing, less typing
In real projects, the thiserror crate generates the Display, Error and From impls from attributes:
#[derive(Debug, thiserror::Error)]
enum ConfigError {
#[error("missing key `{0}`")]
Missing(String),
#[error("not a number")]
BadNumber(#[from] std::num::ParseIntError),
}
Libraries typically expose thiserror enums; applications often wrap everything in anyhow::Error at the top.
Common mistakes
- One giant error enum for the whole program. Give each module or layer its own, and convert at the boundaries.
- Losing the cause: mapping errors to strings drops the source chain. Keep the original error in a field.
- Forgetting
Debug: theErrortrait requires bothDebugandDisplay.
Example
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
Missing(String),
BadNumber { key: String, source: ParseIntError },
OutOfRange(u32),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Missing(key) => write!(f, "missing key `{key}`"),
ConfigError::BadNumber { key, .. } => write!(f, "`{key}` is not a number"),
ConfigError::OutOfRange(n) => write!(f, "{n} is out of range 1-65535"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::BadNumber { source, .. } => Some(source),
_ => None,
}
}
}
fn get<'a>(pairs: &[(&str, &'a str)], key: &str) -> Result<&'a str, ConfigError> {
pairs.iter().find(|(k, _)| *k == key).map(|(_, v)| *v).ok_or_else(|| ConfigError::Missing(key.to_string()))
}
fn port(pairs: &[(&str, &str)]) -> Result<u16, ConfigError> {
let raw = get(pairs, "port")?;
let n: u32 = raw.parse().map_err(|e| ConfigError::BadNumber { key: "port".to_string(), source: e })?;
u16::try_from(n).ok().filter(|p| *p > 0).ok_or(ConfigError::OutOfRange(n))
}
fn main() {
let configs = [vec![("port", "8080")], vec![("host", "arena")], vec![("port", "eighty")], vec![("port", "70000")]];
for c in &configs {
match port(c) {
Ok(p) => println!("port {p}"),
Err(e) => {
print!("error: {e}");
if let Some(cause) = e.source() {
print!(" (caused by: {cause})");
}
println!();
}
}
}
}
Output:
port 8080
error: missing key `port`
error: `port` is not a number (caused by: invalid digit found in string)
error: 70000 is out of range 1-65535