RustArena

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

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

Practice it