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 tests

Key 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.