ExamplesBy LevelBy TopicLearning Paths
317 Intermediate

317: Parse Error Handling

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "317: Parse Error Handling" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Parsing user-provided strings is the entry point for most validation errors. Key difference from OCaml: 1. **Standard interface**: Rust's `FromStr` is a standard trait — implementing it gives `str::parse()` syntax for free; OCaml requires custom `of_string` functions.

Tutorial

The Problem

Parsing user-provided strings is the entry point for most validation errors. The standard library's str::parse::<T>() returns Result<T, <T as FromStr>::Error>, but the default error messages are often too vague. Implementing FromStr for custom types with detailed error enums provides precise, informative error messages and integrates with the standard parse() interface. This is the "type-safe parsing" pattern used throughout production Rust code.

🎯 Learning Outcomes

  • • Implement FromStr for a custom type to enable .parse::<MyType>() syntax
  • • Define detailed parse error enums with Empty, InvalidFormat, and OutOfRange variants
  • • Return specific error variants with context rather than generic string errors
  • • Use FromStr to integrate with the standard library's parsing infrastructure
  • Code Example

    #![allow(clippy::all)]
    //! # Parse Error Handling
    //!
    //! Custom FromStr implementation with detailed error types.
    
    use std::fmt;
    use std::str::FromStr;
    
    #[derive(Debug, PartialEq)]
    pub enum ParsePositiveError {
        Empty,
        InvalidNumber(String),
        NotPositive(i64),
    }
    
    impl fmt::Display for ParsePositiveError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Empty => write!(f, "empty input"),
                Self::InvalidNumber(s) => write!(f, "not a number: {s}"),
                Self::NotPositive(n) => write!(f, "{n} is not positive"),
            }
        }
    }
    
    #[derive(Debug, PartialEq)]
    pub struct PositiveInt(pub u64);
    
    impl FromStr for PositiveInt {
        type Err = ParsePositiveError;
    
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            if s.is_empty() {
                return Err(ParsePositiveError::Empty);
            }
            let n: i64 = s
                .parse()
                .map_err(|_| ParsePositiveError::InvalidNumber(s.to_string()))?;
            if n <= 0 {
                return Err(ParsePositiveError::NotPositive(n));
            }
            Ok(PositiveInt(n as u64))
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid() {
            assert_eq!("42".parse::<PositiveInt>().unwrap().0, 42);
        }
    
        #[test]
        fn test_rejects_zero() {
            assert_eq!(
                "0".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(0)
            );
        }
    
        #[test]
        fn test_rejects_empty() {
            assert_eq!(
                "".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::Empty
            );
        }
    
        #[test]
        fn test_rejects_text() {
            assert!(matches!(
                "abc".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::InvalidNumber(_)
            ));
        }
    
        #[test]
        fn test_negative() {
            assert_eq!(
                "-5".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(-5)
            );
        }
    }

    Key Differences

  • Standard interface: Rust's FromStr is a standard trait — implementing it gives str::parse() syntax for free; OCaml requires custom of_string functions.
  • Error type: type Err = ParsePositiveError makes the error type explicit in the trait; OCaml's return type carries it but without a standard name.
  • Ecosystem integration: FromStr integrates with structopt/clap for CLI argument parsing, serde for deserialization, and reqwest for header value parsing.
  • Blanket impls: All primitive types implement FromStr in Rust's standard library; OCaml provides *_of_string functions for primitives.
  • OCaml Approach

    OCaml uses manual parsing functions rather than a standard parse() interface. The idiomatic approach is a of_string function returning option or result:

    let positive_of_string s =
      if String.length s = 0 then Error `Empty
      else match int_of_string_opt s with
      | None -> Error (`InvalidNumber s)
      | Some n when n <= 0 -> Error (`NotPositive n)
      | Some n -> Ok n
    

    Full Source

    #![allow(clippy::all)]
    //! # Parse Error Handling
    //!
    //! Custom FromStr implementation with detailed error types.
    
    use std::fmt;
    use std::str::FromStr;
    
    #[derive(Debug, PartialEq)]
    pub enum ParsePositiveError {
        Empty,
        InvalidNumber(String),
        NotPositive(i64),
    }
    
    impl fmt::Display for ParsePositiveError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Empty => write!(f, "empty input"),
                Self::InvalidNumber(s) => write!(f, "not a number: {s}"),
                Self::NotPositive(n) => write!(f, "{n} is not positive"),
            }
        }
    }
    
    #[derive(Debug, PartialEq)]
    pub struct PositiveInt(pub u64);
    
    impl FromStr for PositiveInt {
        type Err = ParsePositiveError;
    
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            if s.is_empty() {
                return Err(ParsePositiveError::Empty);
            }
            let n: i64 = s
                .parse()
                .map_err(|_| ParsePositiveError::InvalidNumber(s.to_string()))?;
            if n <= 0 {
                return Err(ParsePositiveError::NotPositive(n));
            }
            Ok(PositiveInt(n as u64))
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid() {
            assert_eq!("42".parse::<PositiveInt>().unwrap().0, 42);
        }
    
        #[test]
        fn test_rejects_zero() {
            assert_eq!(
                "0".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(0)
            );
        }
    
        #[test]
        fn test_rejects_empty() {
            assert_eq!(
                "".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::Empty
            );
        }
    
        #[test]
        fn test_rejects_text() {
            assert!(matches!(
                "abc".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::InvalidNumber(_)
            ));
        }
    
        #[test]
        fn test_negative() {
            assert_eq!(
                "-5".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(-5)
            );
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid() {
            assert_eq!("42".parse::<PositiveInt>().unwrap().0, 42);
        }
    
        #[test]
        fn test_rejects_zero() {
            assert_eq!(
                "0".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(0)
            );
        }
    
        #[test]
        fn test_rejects_empty() {
            assert_eq!(
                "".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::Empty
            );
        }
    
        #[test]
        fn test_rejects_text() {
            assert!(matches!(
                "abc".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::InvalidNumber(_)
            ));
        }
    
        #[test]
        fn test_negative() {
            assert_eq!(
                "-5".parse::<PositiveInt>().unwrap_err(),
                ParsePositiveError::NotPositive(-5)
            );
        }
    }

    Deep Comparison

    parse-error-handling

    See README.md for details.

    Exercises

  • Implement FromStr for an IpAddress type that parses "x.x.x.x" notation, with specific error variants for wrong format, invalid octets, and out-of-range values.
  • Add Display and std::error::Error implementations to your ParsePositiveError type.
  • Use the #[derive(Debug)] and implemented FromStr together with clap to parse a custom CLI argument type.
  • Open Source Repos