050 — Custom Error Types
Tutorial Video
Text description (accessibility)
This video demonstrates the "050 — Custom Error Types" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Custom error types make error handling self-documenting. Key difference from OCaml: 1. **Trait implementation**: Rust requires implementing `Display` and `Error` traits to integrate with the ecosystem. OCaml errors are plain algebraic types — no trait/interface needed, but also no standard display mechanism.
Tutorial
The Problem
Custom error types make error handling self-documenting. Instead of returning String errors (which lose structure) or Box<dyn Error> (which loses type safety), a custom enum provides exhaustive matching, compiler-checked error handling, and rich error messages. This is how Rust's standard library and major crates (serde, tokio, axum) define their errors.
The std::error::Error trait requires implementing Display and optionally source() (for error chaining). The Display implementation provides human-readable messages. Combined with From conversions (example 049), custom error types are the foundation of production Rust error handling.
🎯 Learning Outcomes
std::fmt::Display for human-readable error messagesstd::error::Error to integrate with the Rust ecosystem? and map_err in real functionssource() method for error chaining (wrapping underlying causes)fmt::Display for human-readable messagesstd::error::Error and optionally source() for error chaining with the underlying causeCode Example
#![allow(clippy::all)]
// A custom error enum with Display + std::error::Error for self-documenting failures.
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum ValidationError {
NegativeAge(i32),
UnreasonableAge(i32),
EmptyName,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::NegativeAge(n) => write!(f, "negative age: {}", n),
ValidationError::UnreasonableAge(n) => write!(f, "unreasonable age: {}", n),
ValidationError::EmptyName => write!(f, "name cannot be empty"),
}
}
}
impl std::error::Error for ValidationError {}
pub fn validate_age(age: i32) -> Result<i32, ValidationError> {
if age < 0 {
Err(ValidationError::NegativeAge(age))
} else if age > 150 {
Err(ValidationError::UnreasonableAge(age))
} else {
Ok(age)
}
}
pub fn validate_name(name: &str) -> Result<&str, ValidationError> {
if name.is_empty() {
Err(ValidationError::EmptyName)
} else {
Ok(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_age_ok() {
assert_eq!(validate_age(30), Ok(30));
}
#[test]
fn test_validate_age_negative() {
assert_eq!(validate_age(-1), Err(ValidationError::NegativeAge(-1)));
}
#[test]
fn test_validate_age_unreasonable() {
assert_eq!(validate_age(200), Err(ValidationError::UnreasonableAge(200)));
}
#[test]
fn test_validate_name() {
assert_eq!(validate_name("Ada"), Ok("Ada"));
assert_eq!(validate_name(""), Err(ValidationError::EmptyName));
}
#[test]
fn test_display_messages() {
assert_eq!(ValidationError::NegativeAge(-5).to_string(), "negative age: -5");
assert_eq!(ValidationError::UnreasonableAge(200).to_string(), "unreasonable age: 200");
assert_eq!(ValidationError::EmptyName.to_string(), "name cannot be empty");
}
#[test]
fn test_is_a_std_error() {
let e: Box<dyn std::error::Error> = Box::new(ValidationError::EmptyName);
assert_eq!(e.to_string(), "name cannot be empty");
}
}Key Differences
Display and Error traits to integrate with the ecosystem. OCaml errors are plain algebraic types — no trait/interface needed, but also no standard display mechanism.Error::source() returns the underlying cause. OCaml achieves this by embedding the cause in the error variant: type error = IoFailed of exn | ....thiserror::derive(Error), Rust can derive Display and Error from attribute annotations. OCaml has no standard equivalent; writing to_string functions manually is typical.match is checked at compile time — adding a new variant requires handling it everywhere it is matched.enum for error variants:** A custom error type is an enum — each variant represents a different failure mode. This allows callers to pattern-match on specific errors and handle them differently.Display for human-readable messages:** Implementing fmt::Display for the error type provides the human-readable message shown in logs and user interfaces. Debug is for developers; Display is for users.Error trait:** Implementing std::error::Error marks the type as an error type. It requires Debug + Display. The source() method provides error chaining — linking to the underlying cause.type error = InvalidInput | NetworkError of string | ParseError of int * string. Pattern matching handles each case. OCaml doesn't have a standard Error trait — error handling style varies by library.OCaml Approach
OCaml's equivalent: define type validation_error = NegativeAge of int | UnreasonableAge of int | EmptyName. The Display equivalent is a to_string function: let error_to_string = function NegativeAge n -> Printf.sprintf "negative age: %d" n | .... OCaml has no Error trait — errors are just values. The Printexc module handles exception pretty-printing separately.
Full Source
#![allow(clippy::all)]
// A custom error enum with Display + std::error::Error for self-documenting failures.
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum ValidationError {
NegativeAge(i32),
UnreasonableAge(i32),
EmptyName,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::NegativeAge(n) => write!(f, "negative age: {}", n),
ValidationError::UnreasonableAge(n) => write!(f, "unreasonable age: {}", n),
ValidationError::EmptyName => write!(f, "name cannot be empty"),
}
}
}
impl std::error::Error for ValidationError {}
pub fn validate_age(age: i32) -> Result<i32, ValidationError> {
if age < 0 {
Err(ValidationError::NegativeAge(age))
} else if age > 150 {
Err(ValidationError::UnreasonableAge(age))
} else {
Ok(age)
}
}
pub fn validate_name(name: &str) -> Result<&str, ValidationError> {
if name.is_empty() {
Err(ValidationError::EmptyName)
} else {
Ok(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_age_ok() {
assert_eq!(validate_age(30), Ok(30));
}
#[test]
fn test_validate_age_negative() {
assert_eq!(validate_age(-1), Err(ValidationError::NegativeAge(-1)));
}
#[test]
fn test_validate_age_unreasonable() {
assert_eq!(validate_age(200), Err(ValidationError::UnreasonableAge(200)));
}
#[test]
fn test_validate_name() {
assert_eq!(validate_name("Ada"), Ok("Ada"));
assert_eq!(validate_name(""), Err(ValidationError::EmptyName));
}
#[test]
fn test_display_messages() {
assert_eq!(ValidationError::NegativeAge(-5).to_string(), "negative age: -5");
assert_eq!(ValidationError::UnreasonableAge(200).to_string(), "unreasonable age: 200");
assert_eq!(ValidationError::EmptyName.to_string(), "name cannot be empty");
}
#[test]
fn test_is_a_std_error() {
let e: Box<dyn std::error::Error> = Box::new(ValidationError::EmptyName);
assert_eq!(e.to_string(), "name cannot be empty");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_age_ok() {
assert_eq!(validate_age(30), Ok(30));
}
#[test]
fn test_validate_age_negative() {
assert_eq!(validate_age(-1), Err(ValidationError::NegativeAge(-1)));
}
#[test]
fn test_validate_age_unreasonable() {
assert_eq!(validate_age(200), Err(ValidationError::UnreasonableAge(200)));
}
#[test]
fn test_validate_name() {
assert_eq!(validate_name("Ada"), Ok("Ada"));
assert_eq!(validate_name(""), Err(ValidationError::EmptyName));
}
#[test]
fn test_display_messages() {
assert_eq!(ValidationError::NegativeAge(-5).to_string(), "negative age: -5");
assert_eq!(ValidationError::UnreasonableAge(200).to_string(), "unreasonable age: 200");
assert_eq!(ValidationError::EmptyName.to_string(), "name cannot be empty");
}
#[test]
fn test_is_a_std_error() {
let e: Box<dyn std::error::Error> = Box::new(ValidationError::EmptyName);
assert_eq!(e.to_string(), "name cannot be empty");
}
}
Deep Comparison
OCaml vs Rust: Custom Error Types
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
NetworkError { message: String, cause: Box<dyn std::error::Error> } variant to your error type and implement source() to return Some(cause.as_ref()). Write a function that wraps an underlying error."expected age 0-150, got 200". Use write!(f, ...) with multiple fields from the variant.ApiError type for a web API with variants for NotFound { resource: String }, Unauthorized, RateLimit { retry_after_secs: u64 }, and Internal(Box<dyn std::error::Error>). Implement Display, Error, and HTTP status code mapping.DbError, ApiError (which wraps DbError), and AppError (which wraps ApiError). Implement From conversions so ? works at each level.fn suggestions(&self) -> Vec<String> method to your custom error type that returns actionable suggestions for each error variant — e.g., for InvalidInput suggest checking the input format.