294: Custom Error Types
Tutorial Video
Text description (accessibility)
This video demonstrates the "294: Custom Error Types" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Generic error strings (`String`, `&str`) lose information — callers cannot match on the error kind to handle different failures differently. Key difference from OCaml: 1. **Trait obligation**: Rust error types must implement `Display` and `Debug`; OCaml has no such requirement — any type can be an error.
Tutorial
The Problem
Generic error strings (String, &str) lose information — callers cannot match on the error kind to handle different failures differently. Custom error enums document every possible failure mode in the type system, enabling exhaustive handling, machine-readable error codes, and structured error data. This is the standard approach in production Rust libraries and mirrors OCaml's algebraic error types.
🎯 Learning Outcomes
Display for user-facing error messages and Debug for developer diagnosticsimpl std::error::Error to integrate with the Rust error ecosystemCode Example
#[derive(Debug, PartialEq)]
enum ParseError {
InvalidNumber(String),
OutOfRange { value: i64, min: i64, max: i64 },
EmptyInput,
}Key Differences
Display and Debug; OCaml has no such requirement — any type can be an error.std::error::Error makes Rust errors compatible with Box<dyn Error>, anyhow, and thiserror.OCaml Approach
OCaml uses polymorphic variants or regular variant types for errors, commonly with a single error type defined per module:
type parse_error =
| InvalidNumber of string
| OutOfRange of { value: int; min: int; max: int }
| EmptyInput
let display_error = function
| InvalidNumber s -> Printf.sprintf "invalid number: '%s'" s
| OutOfRange {value; min; max} ->
Printf.sprintf "value %d out of range [%d, %d]" value min max
| EmptyInput -> "empty input"
Full Source
#![allow(clippy::all)]
//! # Custom Error Types
//!
//! Custom error enums document failure modes in the type system.
use std::fmt;
/// All the ways parsing a bounded integer can fail
#[derive(Debug, PartialEq)]
pub enum ParseError {
InvalidNumber(String),
OutOfRange { value: i64, min: i64, max: i64 },
EmptyInput,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::InvalidNumber(s) => write!(f, "invalid number: '{}'", s),
ParseError::OutOfRange { value, min, max } => {
write!(f, "value {} out of range [{}, {}]", value, min, max)
}
ParseError::EmptyInput => write!(f, "empty input"),
}
}
}
/// Parse a string into a bounded integer
pub fn parse_bounded(s: &str, min: i64, max: i64) -> Result<i64, ParseError> {
if s.is_empty() {
return Err(ParseError::EmptyInput);
}
let n: i64 = s
.parse()
.map_err(|_| ParseError::InvalidNumber(s.to_string()))?;
if n < min || n > max {
return Err(ParseError::OutOfRange { value: n, min, max });
}
Ok(n)
}
/// Parse a percentage (0-100)
pub fn parse_percentage(s: &str) -> Result<i64, ParseError> {
parse_bounded(s, 0, 100)
}
/// Parse a port number (1-65535)
pub fn parse_port(s: &str) -> Result<u16, ParseError> {
parse_bounded(s, 1, 65535).map(|n| n as u16)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid() {
assert_eq!(parse_bounded("42", 0, 100), Ok(42));
}
#[test]
fn test_invalid_number() {
assert!(matches!(
parse_bounded("abc", 0, 100),
Err(ParseError::InvalidNumber(_))
));
}
#[test]
fn test_out_of_range_high() {
assert!(matches!(
parse_bounded("200", 0, 100),
Err(ParseError::OutOfRange { value: 200, .. })
));
}
#[test]
fn test_out_of_range_low() {
assert!(matches!(
parse_bounded("-5", 0, 100),
Err(ParseError::OutOfRange { value: -5, .. })
));
}
#[test]
fn test_empty() {
assert_eq!(parse_bounded("", 0, 100), Err(ParseError::EmptyInput));
}
#[test]
fn test_percentage_valid() {
assert_eq!(parse_percentage("50"), Ok(50));
}
#[test]
fn test_percentage_invalid() {
assert!(matches!(
parse_percentage("150"),
Err(ParseError::OutOfRange { .. })
));
}
#[test]
fn test_port_valid() {
assert_eq!(parse_port("8080"), Ok(8080));
}
#[test]
fn test_error_display() {
let err = ParseError::OutOfRange {
value: 200,
min: 0,
max: 100,
};
assert_eq!(format!("{}", err), "value 200 out of range [0, 100]");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid() {
assert_eq!(parse_bounded("42", 0, 100), Ok(42));
}
#[test]
fn test_invalid_number() {
assert!(matches!(
parse_bounded("abc", 0, 100),
Err(ParseError::InvalidNumber(_))
));
}
#[test]
fn test_out_of_range_high() {
assert!(matches!(
parse_bounded("200", 0, 100),
Err(ParseError::OutOfRange { value: 200, .. })
));
}
#[test]
fn test_out_of_range_low() {
assert!(matches!(
parse_bounded("-5", 0, 100),
Err(ParseError::OutOfRange { value: -5, .. })
));
}
#[test]
fn test_empty() {
assert_eq!(parse_bounded("", 0, 100), Err(ParseError::EmptyInput));
}
#[test]
fn test_percentage_valid() {
assert_eq!(parse_percentage("50"), Ok(50));
}
#[test]
fn test_percentage_invalid() {
assert!(matches!(
parse_percentage("150"),
Err(ParseError::OutOfRange { .. })
));
}
#[test]
fn test_port_valid() {
assert_eq!(parse_port("8080"), Ok(8080));
}
#[test]
fn test_error_display() {
let err = ParseError::OutOfRange {
value: 200,
min: 0,
max: 100,
};
assert_eq!(format!("{}", err), "value 200 out of range [0, 100]");
}
}
Deep Comparison
OCaml vs Rust: Custom Error Types
Pattern 1: Error Enum Definition
OCaml
type parse_error =
| InvalidNumber of string
| OutOfRange of int * int * int
| EmptyInput
Rust
#[derive(Debug, PartialEq)]
enum ParseError {
InvalidNumber(String),
OutOfRange { value: i64, min: i64, max: i64 },
EmptyInput,
}
Pattern 2: Error Display
OCaml
let pp_parse_error = function
| InvalidNumber s -> Printf.sprintf "invalid: '%s'" s
| OutOfRange (n, lo, hi) ->
Printf.sprintf "%d out of range [%d, %d]" n lo hi
| EmptyInput -> "empty input"
Rust
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::InvalidNumber(s) => write!(f, "invalid: '{}'", s),
ParseError::OutOfRange { value, min, max } =>
write!(f, "{} out of range [{}, {}]", value, min, max),
ParseError::EmptyInput => write!(f, "empty input"),
}
}
}
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Error type | Variant type or exception | Enum with named variants |
| Display | Ad-hoc function | impl Display trait |
| Debug | Automatic | #[derive(Debug)] |
| Named fields | Tuples only | Struct-like variants available |
| Exhaustiveness | Compiler checks | Compiler checks match arms |
Exercises
NetworkError enum with variants for connection refused, timeout, and authentication failure — each carrying relevant context.ValidationError type with variants for each field constraint violation and aggregate multiple validation failures.source() -> Option<&dyn Error> implementation to wrap a lower-level ParseError inside a higher-level ConfigError.