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