rust 107 lines · 3 tabs

Custom Error Enum With From Conversions for the ? Operator in Rust

Shared by codesnips Jul 2026
3 tabs
use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
pub enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
    Missing(String),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "failed to read config: {}", e),
            ConfigError::Parse(e) => write!(f, "invalid numeric value: {}", e),
            ConfigError::Missing(key) => write!(f, "missing required key: {}", key),
        }
    }
}

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

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

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::Parse(e)
    }
}
3 files · rust Explain with highlit

This snippet shows the canonical way to build a domain-specific error type in Rust so that the ? operator can transparently bubble up errors from different libraries. The core idea is that ? on a Result<T, E> calls From::from on the error before returning it, so any error type that implements From<SourceError> for the function's declared error type will convert automatically. Defining one enum that gathers every failure mode a subsystem can produce, plus the right From impls, turns noisy match-and-map boilerplate into terse fallible code.

In error.rs, ConfigError is an enum with one variant per underlying failure: Io wraps std::io::Error, Parse wraps a std::num::ParseIntError, and Missing carries a plain String for a domain rule violation. Implementing Display gives human-readable messages, and implementing std::error::Error with a source method preserves the underlying cause so callers can walk the error chain. The source implementation returns Some(&dyn Error) for the wrapped variants and None for the domain-only Missing variant, which is exactly what tools like anyhow or logging layers expect.

The two From impls are what make ? ergonomic: From<io::Error> and From<ParseIntError> let the compiler insert the conversion at each ? site. Writing these by hand is the manual equivalent of what the thiserror crate generates, and understanding it clarifies why ? "just works" once the conversions exist.

In config.rs, Config::load demonstrates the payoff. fs::read_to_string(path)? can fail with an io::Error, .parse::<u16>()? can fail with a ParseIntError, and both are converted into ConfigError on the fly because the function returns Result<Config, ConfigError>. The ok_or_else call produces a ConfigError::Missing directly for the business rule that port must exist, showing that not every error needs a From impl. A key trade-off: a single flat enum couples callers to every possible variant, so very large systems sometimes split errors per module or use boxed trait objects instead. For a focused subsystem this explicit enum stays readable, gives exhaustive matching, and keeps zero-cost conversions at every ?.


Related snips

Share this code

Here's the card — post it anywhere.

Custom Error Enum With From Conversions for the ? Operator in Rust — share card
Link copied