317: Parse Error Handling
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
FromStr for a custom type to enable .parse::<MyType>() syntaxEmpty, InvalidFormat, and OutOfRange variantsFromStr to integrate with the standard library's parsing infrastructureCode 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
FromStr is a standard trait — implementing it gives str::parse() syntax for free; OCaml requires custom of_string functions.type Err = ParsePositiveError makes the error type explicit in the trait; OCaml's return type carries it but without a standard name.FromStr integrates with structopt/clap for CLI argument parsing, serde for deserialization, and reqwest for header value parsing.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)
);
}
}#[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
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.Display and std::error::Error implementations to your ParsePositiveError type.#[derive(Debug)] and implemented FromStr together with clap to parse a custom CLI argument type.