ExamplesBy LevelBy TopicLearning Paths
294 Intermediate

294: Custom Error Types

Functional Programming

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

  • • Define error types as enums with variants carrying relevant context data
  • • Implement Display for user-facing error messages and Debug for developer diagnostics
  • • Use impl std::error::Error to integrate with the Rust error ecosystem
  • • Recognize when struct variants (with named fields) vs tuple variants are appropriate
  • Code Example

    #[derive(Debug, PartialEq)]
    enum ParseError {
        InvalidNumber(String),
        OutOfRange { value: i64, min: i64, max: i64 },
        EmptyInput,
    }

    Key Differences

  • Trait obligation: Rust error types must implement Display and Debug; OCaml has no such requirement — any type can be an error.
  • Ecosystem integration: Implementing std::error::Error makes Rust errors compatible with Box<dyn Error>, anyhow, and thiserror.
  • Structured data: Both languages support carrying context data in error variants with field-carrying structs or tuples.
  • Exhaustive matching: Both Rust and OCaml require exhaustive match on error variants — adding a variant is a compile-time breaking change.
  • 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]");
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Error typeVariant type or exceptionEnum with named variants
    DisplayAd-hoc functionimpl Display trait
    DebugAutomatic#[derive(Debug)]
    Named fieldsTuples onlyStruct-like variants available
    ExhaustivenessCompiler checksCompiler checks match arms

    Exercises

  • Define a NetworkError enum with variants for connection refused, timeout, and authentication failure — each carrying relevant context.
  • Implement a ValidationError type with variants for each field constraint violation and aggregate multiple validation failures.
  • Add a source() -> Option<&dyn Error> implementation to wrap a lower-level ParseError inside a higher-level ConfigError.
  • Open Source Repos