What is the difference between nom::error::Error and VerboseError for parser error reporting and debugging?
nom::error::Error provides minimal error information—just the input position where parsing failed—while VerboseError accumulates context at each failure point, building a chain of error locations that helps trace through complex parser combinations to understand exactly where and why parsing stopped. This trade-off between performance and debugging capability means Error is suitable for production parsers where you only need success/failure, while VerboseError is essential during development or when you need meaningful error messages for users.
Basic Error Type
use nom::{IResult, error::{Error, ParseError}, bytes::complete::tag, character::complete::alpha1};
fn basic_error_example() {
// Error<T> contains just the input position
type SimpleError<'a> = Error<&'a str>;
fn parse_word(input: &str) -> IResult<&str, &str, SimpleError<'_>> {
tag("hello")(input)
}
let result = parse_word("world");
match result {
Err(nom::Err::Error(Error { input, .. })) => {
println!("Failed at: '{}'", input); // "world"
// No other information available
}
_ => {}
}
}Error<T> stores only the remaining input at the failure point.
VerboseError Structure
use nom::{IResult, error::VerboseError, bytes::complete::tag, sequence::tuple};
fn verbose_error_example() {
// VerboseError stores a list of (input, error_kind) pairs
type VError<'a> = VerboseError<&'a str>;
fn parse_greeting(input: &str) -> IResult<&str, &str, VError<'_>> {
let (input, _) = tag("hello")(input)?;
let (input, _) = tag(" ")(input)?;
tag("world")(input)
}
let result = parse_greeting("hi there");
match result {
Err(nom::Err::Error(e)) => {
// VerboseError contains:
// - errors: Vec<(input, VerboseErrorKind)>
// - Each failure point is recorded
println!("Verbose error: {:?}", e.errors);
}
_ => {}
}
}VerboseError accumulates all failure points with context.
The errors Field
use nom::error::VerboseError;
fn errors_field() {
// VerboseError has:
// pub struct VerboseError<I> {
// pub errors: Vec<(I, VerboseErrorKind)>,
// }
// VerboseErrorKind can be:
// - Context(&'static str): Named context
// - Nom(ErrorKind): Standard nom error
// - Char(char): Expected character
// - Kind(ErrorKind): Error kind
// The vector grows as errors are added through parser combinators
// Each backtracking point adds an entry
}errors is a vector of failure contexts, built as parsing backtracks.
Context for Better Messages
use nom::{IResult, error::{VerboseError, VerboseErrorKind, context},
bytes::complete::tag, sequence::preceded, character::complete::alpha1};
fn with_context() {
fn parse_name(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
context(
"name",
preceded(
tag("name: "),
alpha1
)
)(input)
}
fn parse_record(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
context(
"record",
parse_name
)(input)
}
let result = parse_record("name: 123");
match result {
Err(nom::Err::Error(e)) => {
// e.errors contains entries for both "record" and "name" contexts
for (input, kind) in e.errors {
println!("Error at '{}' with kind {:?}", input, kind);
}
}
_ => {}
}
}context() adds named markers to the error chain for tracing.
Comparing Error and VerboseError
use nom::{IResult, error::{Error, VerboseError, ParseError},
bytes::complete::tag, sequence::tuple};
fn comparison() {
type SimpleErr<'a> = Error<&'a str>;
type VerboseErr<'a> = VerboseError<&'a str>;
fn parse_with_simple(input: &str) -> IResult<&str, (&str, &str), SimpleErr<'_>> {
tuple((
tag("hello"),
tag(" "),
tag("world")
))(input)
}
fn parse_with_verbose(input: &str) -> IResult<&str, (&str, &str, &str), VerboseErr<'_>> {
tuple((
tag("hello"),
tag(" "),
tag("world")
))(input)
}
// With Error:
let result1 = parse_with_simple("hello world");
// Error contains: remaining input at first failure
// Single point of information
// With VerboseError:
let result2 = parse_with_verbose("hello world");
// VerboseError contains: chain of failures
// Multiple points of information
}Error gives one failure point; VerboseError gives the full chain.
The ParseError Trait
use nom::error::{ParseError, ErrorKind};
fn parse_error_trait() {
// Both Error and VerboseError implement ParseError
// Required methods:
// from_error_kind: Create error from position and kind
// append: Add another error to the chain
// from_char: Create error from expected character
// Error<T> implements ParseError minimally:
// - from_error_kind: Creates Error { input, code }
// - append: Replaces with new error (loses old)
// - from_char: Creates Error { input, code: Char }
// VerboseError<T> implements ParseError richly:
// - from_error_kind: Creates VerboseError { errors: [(input, kind)] }
// - append: Pushes new error to errors vector
// - from_char: Pushes (input, Char(c)) to errors
// The key difference: append
// Error::append drops the previous error
// VerboseError::append accumulates errors
}The append method is where VerboseError builds its chain.
ErrorAccumulator Pattern
use nom::{IResult, error::{VerboseError, VerboseErrorKind},
bytes::complete::tag, alt, sequence::tuple};
fn error_accumulation() {
// When alt tries multiple parsers and all fail:
fn parse_option(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
alt((
tag("option_a"),
tag("option_b"),
tag("option_c")
))(input)
}
let result = parse_option("unknown");
match result {
Err(nom::Err::Error(e)) => {
// e.errors contains ALL failures:
// - tag("option_a") failed
// - tag("option_b") failed
// - tag("option_c") failed
// Each adds to the error chain
for (input, kind) in &e.errors {
println!("Failed at '{}' with {:?}", input, kind);
}
}
_ => {}
}
}alt accumulates errors from each alternative, showing all attempted matches.
Formatting VerboseError
use nom::error::VerboseError;
fn format_verbose_error() {
fn convert_error(input: &str, e: VerboseError<&str>) -> String {
use std::fmt::Write;
let mut result = String::new();
for (substring, kind) in e.errors {
match kind {
VerboseErrorKind::Context(ctx) => {
writeln!(result, "In context '{}', input: '{}'", ctx, substring);
}
VerboseErrorKind::Nom(kind) => {
writeln!(result, "Nom error {:?} at: '{}'", kind, substring);
}
VerboseErrorKind::Char(c) => {
writeln!(result, "Expected '{}', found: '{}'", c, substring);
}
_ => {}
}
}
// Find position in original input
if let Some((first_substring, _)) = e.errors.first() {
if let Some(offset) = input.find(first_substring) {
writeln!(result, "Position: {}", offset);
}
}
result
}
}Custom formatting extracts meaningful messages from VerboseError.
Using nom::error::convert_error
use nom::{IResult, error::{VerboseError, convert_error},
bytes::complete::tag, sequence::preceded, character::complete::alpha1, context};
fn convert_error_example() {
fn parse_identifier(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
context(
"identifier",
preceded(
context("prefix", tag("id:")),
context("name", alpha1)
)
)(input)
}
let input = "id: 123"; // alpha1 will fail on digit
let result = parse_identifier(input);
match result {
Err(nom::Err::Error(e)) => {
// convert_error provides a human-readable error string
let error_string = convert_error(input, e);
println!("{}", error_string);
// Output shows context chain and position
}
_ => {}
}
}convert_error creates human-readable error messages from VerboseError.
Error Kind Types
use nom::error::{ErrorKind, VerboseErrorKind};
fn error_kinds() {
// ErrorKind: Standard nom error types
// Used by Error<T>
let kinds = vec![
ErrorKind::Tag, // tag() failed
ErrorKind::Alt, // alt() failed
ErrorKind::MapRes, // map_res() failed
ErrorKind::Many1, // many1() failed
ErrorKind::Count, // count() failed
ErrorKind::Eof, // expected eof
];
// VerboseErrorKind: Extended error types
// Used by VerboseError<T>
enum VerboseErrorKind {
Context(&'static str), // Named context from context()
Nom(ErrorKind), // Wrapped standard error
Char(char), // Expected character from char()
Kind(ErrorKind), // Error kind
}
// VerboseErrorKind can represent both:
// - Standard errors (Nom variant)
// - Custom context (Context variant)
// - Character expectations (Char variant)
}VerboseErrorKind extends ErrorKind with context and character information.
Performance Impact
use nom::{IResult, error::{Error, VerboseError}, bytes::complete::tag};
fn performance_impact() {
// Error<T>: Minimal overhead
// - Single allocation on error
// - Just stores input slice
// - Fast parsing, minimal error info
// VerboseError<T>: More overhead
// - Vec allocation on error
// - Push on each append
// - Accumulates through backtracking
// Rule of thumb:
// - Production parser with known inputs: Error
// - Development/CLI with user input: VerboseError
// - High-throughput parsing: Error
// - Error messages for users: VerboseError
// Performance difference:
// - Error: ~O(1) per error
// - VerboseError: ~O(depth) per error (depth of parser tree)
}VerboseError has overhead for accumulating errors; Error is lightweight.
Combining with map_res
use nom::{IResult, error::{VerboseError, ParseError},
bytes::complete::tag, combinator::map_res};
fn map_res_errors() {
fn parse_number(input: &str) -> IResult<&str, i32, VerboseError<&str>> {
let (input, digits) = tag("num:")(input)?;
// map_res adds error context for conversion failures
map_res(
|i| tag("123")(i), // or digit1
|s: &str| s.parse::<i32>()
)(input)
}
// If parsing fails:
// - VerboseError records the failure
// - If conversion fails:
// - VerboseError records the failure with context
let result = parse_number("num: abc");
// Error chain shows where parsing stopped
}map_res failures are captured in VerboseError's error chain.
Nested Parser Errors
use nom::{IResult, error::VerboseError, bytes::complete::tag,
sequence::tuple, context, branch::alt};
fn nested_errors() {
fn parse_value(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
context("value", alt((
tag("true"),
tag("false"),
tag("null")
)))(input)
}
fn parse_field(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
context("field", tuple((
context("key", tag("name")),
tag(":"),
context("value_parser", parse_value)
)))(input)
}
let result = parse_field("name: invalid");
match result {
Err(nom::Err::Error(e)) => {
// Error chain includes:
// 1. "field" context
// 2. "value_parser" context
// 3. "value" context
// 4. Failed tag attempts for "true", "false", "null"
println!("Errors: {}", e.errors.len());
}
_ => {}
}
}Nested parsers build deep error chains showing the full parse path.
Error Trait Bounds
use nom::{IResult, error::{Error, VerboseError, ParseError, FromExternalError}};
fn trait_bounds() {
// To write generic parsers:
fn generic_parser<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
tag("hello")(input)
}
// Can be used with either Error or VerboseError:
let _ = generic_parser::<Error<&str>>("hello");
let _ = generic_parser::<VerboseError<&str>>("hello");
// ParseError is the minimum bound
// FromExternalError allows wrapping external errors
// ContextError allows context()
fn with_context<'a, E>(input: &'a str) -> IResult<&'a str, &'a str, E>
where
E: ParseError<&'a str> + nom::error::ContextError<&'a str>,
{
context("named", tag("hello"))(input)
}
}Generic parsers can work with either error type using trait bounds.
Converting Between Error Types
use nom::error::{Error, VerboseError, ParseError};
fn conversion() {
// Error -> VerboseError: Create new VerboseError from Error
fn error_to_verbose<I: Clone>(e: Error<I>) -> VerboseError<I> {
VerboseError {
errors: vec![(e.input, nom::error::VerboseErrorKind::Nom(e.code))]
}
}
// VerboseError -> Error: Take only first/last error
fn verbose_to_error<I>(e: VerboseError<I>) -> Error<I> {
// Take the most recent error (last in chain)
let (input, kind) = e.errors.last().unwrap();
Error {
input: input.clone(), // Need Clone for this
code: match kind {
nom::error::VerboseErrorKind::Nom(k) => *k,
nom::error::VerboseErrorKind::Context(_) => nom::error::ErrorKind::Tag,
nom::error::VerboseErrorKind::Char(_) => nom::error::ErrorKind::Tag,
_ => nom::error::ErrorKind::Tag,
}
}
}
// Usually better to pick one error type and use consistently
}Converting loses information; prefer consistent error type usage.
Custom Error Types
use nom::{IResult, error::{ParseError, ErrorKind, FromExternalError}};
// Custom error type for application-specific errors
#[derive(Debug)]
struct AppError<'a> {
input: &'a str,
message: String,
}
impl<'a> ParseError<&'a str> for AppError<'a> {
fn from_error_kind(input: &'a str, kind: ErrorKind) -> Self {
AppError {
input,
message: format!("Parse error: {:?}", kind),
}
}
fn append(_: &'a str, _: ErrorKind, other: Self) -> Self {
other // Like Error, we drop previous errors
}
fn from_char(input: &'a str, c: char) -> Self {
AppError {
input,
message: format!("Expected '{}'", c),
}
}
}
impl<'a> FromExternalError<&'a str, std::num::ParseIntError> for AppError<'a> {
fn from_external_error(input: &'a str, _: ErrorKind, e: std::num::ParseIntError) -> Self {
AppError {
input,
message: format!("Parse int error: {}", e),
}
}
}Custom error types can provide application-specific error messages.
Practical Error Handling
use nom::{IResult, error::VerboseError, bytes::complete::tag,
character::complete::digit1, combinator::map_res, context, convert_error};
fn practical_example() {
fn parse_id(input: &str) -> IResult<&str, u32, VerboseError<&str>> {
context(
"id field",
map_res(digit1, |s: &str| s.parse::<u32>())
)(input)
}
fn parse_record(input: &str) -> IResult<&str, (u32, &str), VerboseError<&str>> {
context(
"record",
|i| {
let (i, id) = context("id", parse_id)(i)?;
let (i, _) = context("separator", tag(","))(i)?;
let (i, name) = context("name", tag("Alice"))(i)?;
Ok((i, (id, name)))
}
)(input)
}
fn run_parser(input: &str) -> Result<(u32, &str), String> {
match parse_record(input) {
Ok((_, result)) => Ok(result),
Err(nom::Err::Error(e)) => Err(convert_error(input, e)),
Err(nom::Err::Failure(e)) => Err(convert_error(input, e)),
Err(nom::Err::Incomplete(_)) => Err("Need more input".to_string()),
}
}
// Usage
match run_parser("12,Bob") {
Ok(result) => println!("Parsed: {:?}", result),
Err(e) => println!("Error:\n{}", e),
}
}convert_error and context together provide user-friendly error messages.
Synthesis
Comparison table:
| Aspect | Error<I> |
VerboseError<I> |
|---|---|---|
| Fields | input, code |
errors: Vec<(I, VerboseErrorKind)> |
| Information | Single failure point | Chain of failures |
| Performance | Minimal overhead | Accumulation overhead |
| Memory | One allocation | Vector allocation |
| Use case | Production, high-throughput | Development, debugging |
| Error messages | Basic (need manual formatting) | Rich (with convert_error) |
append behavior |
Replaces previous | Accumulates |
When to use each:
// Use Error<T> when:
// - Production parser with known inputs
// - High-throughput requirements
// - Only need success/failure
// - Memory-constrained environments
// - Simple error handling
// Use VerboseError<T> when:
// - Development/debugging phase
// - User-facing error messages
// - Complex parsers with nested alternatives
// - Need to understand parse failures
// - Writing parser testsKey insight: The difference between Error and VerboseError is fundamentally about information vs. performance. Error captures the minimum—a single input position and error kind—making it fast and lightweight. VerboseError accumulates every failure point through the parser tree, creating a trace of what was tried and where it failed. This makes VerboseError invaluable for debugging complex parsers where the failure point isn't obvious, especially with alt and context combinators that try multiple alternatives. The append method is the key: Error::append discards the previous error (keeping only the most recent), while VerboseError::append pushes to the vector (keeping the history). For parser development, always start with VerboseError and context()—you can switch to Error later if performance matters. For user-facing parsers (CLI tools, configuration parsing), VerboseError with convert_error provides the meaningful error messages users need to fix their input.
