Error Handling

Recoverable Parsing Errors

Recoverable parsing errors occur when converting untrusted text into structured Rust types via str::parse::<T>() (backed by the std::str::FromStr trait). Because external strings can be empty, malformed, or out of range, Rust parsers return Result<T, E> rather than throwing exceptions or returning sentinel values.

Inspecting ParseIntError and IntErrorKind

When parsing integers (i8..i128, u8..u128, isize, usize), str::parse returns Result<T, std::num::ParseIntError>. Calling err.kind() returns &std::num::IntErrorKind, allowing callers to distinguish syntax errors from numeric overflow:

IntErrorKind Variant Cause Example Input for u8::from_str
IntErrorKind::Empty Input string is "" ""
IntErrorKind::InvalidDigit Contains non-digit characters or invalid sign "12a", "-5" (for unsigned u8)
IntErrorKind::PosOverflow Value exceeds T::MAX "300" (u8::MAX is 255)
IntErrorKind::NegOverflow Value is below T::MIN "-200" (for i8, where i8::MIN is -128)
IntErrorKind::Zero Value is 0 for a NonZero* target type "0" (for NonZeroU8)
use std::num::IntErrorKind;

fn parse_u8_clamped(input: &str) -> Result<u8, String> {
    match input.trim().parse::<u8>() {
        Ok(n) => Ok(n),
        Err(e) => match e.kind() {
            IntErrorKind::PosOverflow => Ok(u8::MAX),
            IntErrorKind::Empty => Ok(0),
            _ => Err(format!("cannot parse {input:?} as u8: {e}")),
        },
    }
}

fn main() {
    for sample in ["42", "999", "", "abc"] {
        println!("{sample:?} -> {:?}", parse_u8_clamped(sample));
    }
}
"42" -> Ok(42)
"999" -> Ok(255)
"" -> Ok(0)
"abc" -> Err("cannot parse \"abc\" as u8: invalid digit found in string")

Implementing FromStr for Custom Domain Types

Implementing std::str::FromStr for your own type enables "text".parse::<MyType>() with a dedicated error enum:

use std::num::ParseIntError;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
pub struct KeyValue {
    pub key: String,
    pub count: u32,
}

#[derive(Debug, PartialEq)]
pub enum ParseKvError {
    MissingDelimiter,
    EmptyKey,
    InvalidCount(ParseIntError),
}

impl FromStr for KeyValue {
    type Err = ParseKvError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (raw_key, raw_val) = s.split_once('=').ok_or(ParseKvError::MissingDelimiter)?;
        let key = raw_key.trim();
        if key.is_empty() {
            return Err(ParseKvError::EmptyKey);
        }
        let count = raw_val
            .trim()
            .parse::<u32>()
            .map_err(ParseKvError::InvalidCount)?;

        Ok(KeyValue {
            key: key.to_string(),
            count,
        })
    }
}

fn main() {
    for line in ["retries = 3", "= 5", "timeout = fast", "no_equals"] {
        let parsed: Result<KeyValue, ParseKvError> = line.parse();
        println!("{line:?} -> {parsed:?}");
    }
}
"retries = 3" -> Ok(KeyValue { key: "retries", count: 3 })
"= 5" -> Err(EmptyKey)
"timeout = fast" -> Err(InvalidCount(ParseIntError { kind: InvalidDigit }))
"no_equals" -> Err(MissingDelimiter)

Accumulating Valid Items Alongside Parse Errors

When parsing a batch of records where bad lines should be reported rather than aborting the entire batch, partition the results into a Vec of parsed items and a Vec of (line_number, error) diagnostics:

fn main() {
    let lines = ["100", "250", "bad_line", "400", "99999999999999999999"];
    let mut valid: Vec<u32> = Vec::new();
    let mut errors: Vec<(usize, String)> = Vec::new();

    for (idx, raw) in lines.iter().enumerate() {
        match raw.parse::<u32>() {
            Ok(n) => valid.push(n),
            Err(e) => errors.push((idx + 1, format!("{raw:?}: {e}"))),
        }
    }

    println!("Valid records: {valid:?}");
    for (line_no, msg) in &errors {
        println!("Line {line_no} error: {msg}");
    }
}
Valid records: [100, 250, 400]
Line 3 error: "bad_line": invalid digit found in string
Line 5 error: "99999999999999999999": number too large to fit in target type