SyntaxStudy
Sign Up
Rust Custom Error Types and the thiserror Crate
Rust Beginner 1 min read

Custom Error Types and the thiserror Crate

For library code, returning `Box` loses the concrete error type and makes it hard for callers to distinguish error cases programmatically. The best practice is to define a custom enum error type, implement `std::fmt::Display` and `std::error::Error` for it, and implement `From` for all underlying error types so `?` can convert them automatically. Writing these implementations by hand is repetitive. The `thiserror` crate provides a `#[derive(Error)]` macro that generates correct implementations from attribute annotations. `#[error("message")]` generates `Display`; `#[from]` generates `From`; `#[source]` marks the cause of an error. The `thiserror` approach is the recommended choice for library crates. For application code (binaries), the `anyhow` crate provides a convenient `anyhow::Error` type that wraps any error implementing `std::error::Error` and adds context with the `.context("description")` method. Mixing `thiserror` in your library layers and `anyhow` in your application binary is a very common and idiomatic Rust pattern for structured, ergonomic error handling.
Example
// Cargo.toml dependency: thiserror = "1"
// (Shown here using manual implementations for zero-dependency demo)

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Io(std::io::Error),
    Parse(ParseIntError),
    InvalidRange { value: i64, min: i64, max: i64 },
    MissingField(String),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO error: {e}"),
            ConfigError::Parse(e) => write!(f, "parse error: {e}"),
            ConfigError::InvalidRange { value, min, max } =>
                write!(f, "value {value} is outside range [{min}, {max}]"),
            ConfigError::MissingField(field) =>
                write!(f, "missing required field: {field}"),
        }
    }
}

impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ConfigError::Io(e) => Some(e),
            ConfigError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for ConfigError { fn from(e: std::io::Error) -> Self { ConfigError::Io(e) } }
impl From<ParseIntError> for ConfigError { fn from(e: ParseIntError) -> Self { ConfigError::Parse(e) } }

fn parse_port(s: Option<&str>) -> Result<u16, ConfigError> {
    let s = s.ok_or_else(|| ConfigError::MissingField("port".into()))?;
    let n: i64 = s.parse()?;
    if !(1..=65535).contains(&n) {
        return Err(ConfigError::InvalidRange { value: n, min: 1, max: 65535 });
    }
    Ok(n as u16)
}

fn main() {
    for input in [Some("8080"), Some("99999"), Some("abc"), None] {
        match parse_port(input) {
            Ok(p) => println!("port = {p}"),
            Err(e) => println!("error: {e}"),
        }
    }
}