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