Error Handling

Error Handling with Enums

An error enum is a custom enum whose variants represent the distinct failure conditions a function, module, or crate can encounter. Grouping related failures under an enum lets functions return a single concrete error type Result<T, MyError> while enabling callers to pattern-match on specific failure cases for targeted recovery.

Structuring an Error Enum

An error enum typically combines three kinds of variants:

Variant Kind Example Purpose
Unit variant EmptyInput Represents a self-explanatory condition with no extra payload
Tuple variant ParseInt(ParseIntError) Wraps an underlying error from a lower-level library
Struct variant OutOfRange { value: i32, min: i32, max: i32 } Carries named diagnostic fields for structured recovery
QueryError
  ├── EmptyInput                               --> Unit variant
  ├── InvalidNumber(ParseIntError)             --> Tuple variant wrapping source error
  └── LimitOutOfBounds { limit: u32, max: u32 } --> Struct variant with domain fields

Implementing Display, Error, and From on an Error Enum

use std::error::Error;
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
pub enum LimitError {
    EmptyInput,
    InvalidNumber(ParseIntError),
    LimitOutOfBounds { limit: u32, max: u32 },
}

impl fmt::Display for LimitError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LimitError::EmptyInput => write!(f, "limit string is empty"),
            LimitError::InvalidNumber(e) => write!(f, "invalid limit number: {e}"),
            LimitError::LimitOutOfBounds { limit, max } => {
                write!(f, "limit {limit} exceeds maximum allowed ({max})")
            }
        }
    }
}

impl Error for LimitError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            LimitError::InvalidNumber(e) => Some(e),
            LimitError::EmptyInput | LimitError::LimitOutOfBounds { .. } => None,
        }
    }
}

impl From<ParseIntError> for LimitError {
    fn from(err: ParseIntError) -> Self {
        LimitError::InvalidNumber(err)
    }
}

fn parse_page_limit(raw: &str, max: u32) -> Result<u32, LimitError> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(LimitError::EmptyInput);
    }
    let limit: u32 = trimmed.parse()?; // Uses From<ParseIntError>
    if limit == 0 || limit > max {
        return Err(LimitError::LimitOutOfBounds { limit, max });
    }
    Ok(limit)
}

fn main() {
    for input in ["25", "", "abc", "500"] {
        match parse_page_limit(input, 100) {
            Ok(limit) => println!("Input {input:?} -> accepted limit {limit}"),
            Err(LimitError::EmptyInput) => {
                println!("Input {input:?} -> empty; defaulting to 20")
            }
            Err(LimitError::LimitOutOfBounds { max, .. }) => {
                println!("Input {input:?} -> clamping to max {max}")
            }
            Err(err) => println!("Input {input:?} -> rejected ({err})"),
        }
    }
}
Input "25" -> accepted limit 25
Input "" -> empty; defaulting to 20
Input "abc" -> rejected (invalid limit number: invalid digit found in string)
Input "500" -> clamping to max 100

Preserving Backwards Compatibility with #[non_exhaustive]

Adding a new variant to a public enum is normally a breaking change because downstream match expressions without a wildcard arm (_ => ...) stop compiling. Marking a public error enum with #[non_exhaustive] requires external crates to include a wildcard branch when matching, allowing new error variants to be added in minor releases without breaking callers:

#[non_exhaustive]
#[derive(Debug)]
pub enum StorageError {
    NotFound,
    PermissionDenied,
    Timeout,
}

fn main() {
    let err = StorageError::Timeout;
    match err {
        StorageError::NotFound => println!("missing item"),
        _ => println!("other storage error: {err:?}"),
    }
}
other storage error: Timeout