What is the purpose of nom::error::VerboseError for detailed parser debugging information?

nom::error::VerboseError is an error type that accumulates the complete parsing context at each failure point—recording the input slice, error kind, and parser chain position—enabling rich error messages that show exactly where and why parsing failed. The default nom error type (I, ErrorKind) captures only the input position and error category, losing critical debugging information when parsers compose and backtrack. VerboseError instead builds an error chain as parsing descends through combinators, preserving the context needed to report "expected digit at position 5, but found 'x'" rather than just "invalid digit" at an opaque location. This makes parser development significantly easier: when a complex parser fails, VerboseError reveals the exact combinator and input position where the failure occurred, rather than leaving the developer guessing which branch of a deeply nested parser tree rejected the input.

Basic Error Types in nom

use nom::{IResult, error::{Error, ErrorKind}, bytes::complete::tag, character::complete::digit1};
 
// Default error type: (Input, ErrorKind)
type DefaultError<'a> = (&'a str, ErrorKind);
 
fn parse_number(input: &str) -> IResult<&str, &str> {
    digit1(input)
}
 
fn main() {
    let result = parse_number("abc");
    match result {
        Err(e) => println!("Default error: {:?}", e),
        _ => println!("Success"),
    }
    // Output: Error(("abc", Digit))
}

The default error type shows the input and error kind but loses context.

VerboseError for Rich Messages

use nom::{IResult, bytes::complete::tag, character::complete::digit1};
use nom::error::{VerboseError, context};
 
fn parse_number(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("number", digit1)(input)
}
 
fn main() {
    let result = parse_number("abc");
    match result {
        Err(nom::Err::Error(e)) => {
            println!("Verbose error: {:?}", e);
            // Contains context about where parsing failed
        }
        Err(nom::Err::Failure(e)) => {
            println!("Fatal error: {:?}", e);
        }
        Ok((remaining, output)) => {
            println!("Ok: remaining={}, output={}", remaining, output);
        }
    }
}

VerboseError accumulates context as parsing progresses through combinators.

Understanding VerboseError Structure

use nom::error::VerboseError;
 
fn main() {
    // VerboseError contains:
    // - A vector of (input, VerboseErrorKind) pairs
    // - Each pair represents a failure point in the parsing chain
    
    // VerboseErrorKind has variants:
    // - Context(&'static str): Named context from context() combinator
    // - Nom(ErrorKind): Original nom error kind
    
    let errors: Vec<(&str, nom::error::VerboseErrorKind)> = vec
![];
    let verbose_error = VerboseError { errors };
    
    println!("VerboseError is a Vec of (input, error_kind) pairs");
    println!("This chain allows tracing back through failures");
}

VerboseError stores a chain of failures, each with input position and error kind.

Context Combinator for Named Errors

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::{digit1, alpha1},
    sequence::preceded,
    error::{VerboseError, context},
};
 
fn parse_id(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context(
        "id_parser",
        preceded(
            context("prefix", tag("ID:")),
            context("value", alpha1)
        )
    )(input)
}
 
fn main() {
    let result = parse_id("ID:123");
    match result {
        Err(nom::Err::Error(e)) => {
            println!("Error chain:");
            for (input, kind) in e.errors {
                println!("  at '{}' : {:?}", input, kind);
            }
            // Shows context chain: id_parser -> prefix -> value
            // Points to where "123" failed alpha1
        }
        Ok((remaining, output)) => {
            println!("Parsed: '{}' remaining: '{}'", output, remaining);
        }
        _ => {}
    }
}

The context combinator adds named checkpoints to the error chain.

Comparing Default and Verbose Errors

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::digit1,
    sequence::tuple,
    error::{Error, VerboseError, context},
};
 
// Parser with default error type
fn parse_default(input: &str) -> IResult<&str, (&str, &str), Error<&str>> {
    tuple((tag("hello"), digit1))(input)
}
 
// Parser with verbose error type
fn parse_verbose(input: &str) -> IResult<&str, (&str, &str), VerboseError<&str>> {
    context("full_parser",
        tuple((
            context("greeting", tag("hello")),
            context("number", digit1)
        ))
    )(input)
}
 
fn main() {
    let bad_input = "hello world";
    
    // Default error: minimal information
    match parse_default(bad_input) {
        Err(nom::Err::Error(e)) => {
            println!("Default error:");
            println!("  Input: {:?}", e.input);
            println!("  Kind: {:?}", e.code);
        }
        _ => {}
    }
    
    // Verbose error: full context chain
    match parse_verbose(bad_input) {
        Err(nom::Err::Error(e)) => {
            println!("\nVerbose error:");
            for (input, kind) in e.errors {
                println!("  Input: '{}'", input);
                println!("  Error: {:?}", kind);
                println!("  ---");
            }
        }
        _ => {}
    }
}

Default errors show one failure; verbose errors show the entire failure chain.

Formatting Verbose Errors for Display

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::digit1,
    error::{VerboseError, context},
};
 
fn parse_item(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("item",
        context("item_prefix", tag("item:"))
    )(input)
}
 
fn format_error(input: &str, error: VerboseError<&str>) -> String {
    let mut result = String::new();
    
    for (error_input, error_kind) in error.errors {
        // Find position in original input
        let offset = input.len() - error_input.len();
        
        result.push_str(&format!(
            "at offset {} ({:?}): '{}'\n",
            offset,
            error_kind,
            error_input.chars().take(20).collect::<String>()
        ));
    }
    
    result
}
 
fn main() {
    let input = "item";
    let result = parse_item(input);
    
    match result {
        Err(nom::Err::Error(e)) => {
            println!("{}", format_error(input, e));
        }
        Ok((remaining, matched)) => {
            println!("Matched: '{}', remaining: '{}'", matched, remaining);
        }
        _ => {}
    }
}

Custom formatting extracts offset positions and error context for user messages.

Error Accumulation Through Combinators

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::{alpha1, digit1},
    sequence::tuple,
    branch::alt,
    error::{VerboseError, context},
};
 
fn parse_record(input: &str) -> IResult<&str, (&str, &str), VerboseError<&str>> {
    context("record",
        tuple((
            context("name", alpha1),
            context("separator", tag(",")),
            context("age", digit1)
        ))
    )(input)
}
 
fn main() {
    // Missing separator
    let result = parse_record("John25");
    
    match result {
        Err(nom::Err::Error(e)) => {
            println!("Error chain for 'John25':");
            for (input, kind) in &e.errors {
                println!("  Input '{}' : {:?}", input, kind);
            }
            // Chain shows:
            // 1. separator expected but '2' found
            // 2. record context
            // 3. name succeeded, separator failed
        }
        _ => {}
    }
    
    // Invalid age
    let result = parse_record("John,abc");
    match result {
        Err(nom::Err::Error(e)) => {
            println!("\nError chain for 'John,abc':");
            for (input, kind) in e.errors {
                println!("  Input '{}' : {:?}", input, kind);
            }
        }
        _ => {}
    }
}

Each nested combinator adds context to the error chain as parsing unwinds.

The VerboseErrorKind Enum

use nom::error::VerboseErrorKind;
 
fn main() {
    // VerboseErrorKind has two variants:
    
    // ContextErr: Named context from context() combinator
    let context_err = VerboseErrorKind::Context("user_id");
    println!("Context error: {:?}", context_err);
    
    // Nom: Original nom ErrorKind
    let nom_err = VerboseErrorKind::Nom(nom::error::ErrorKind::Tag);
    println!("Nom error: {:?}", nom_err);
    
    // Common ErrorKind values:
    // - Tag: Expected literal string
    // - Char: Expected specific character
    // - Digit: Expected digit
    // - Alpha: Expected alphabetic character
    // - Many0, Many1: Repetition errors
    // - Alt: All alternatives failed
}

VerboseErrorKind distinguishes between named contexts and base nom errors.

Building a Parser with Detailed Errors

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::{alpha1, digit1, multispace0, char},
    sequence::{preceded, tuple},
    branch::alt,
    error::{VerboseError, context},
};
 
#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}
 
fn parse_person(input: &str) -> IResult<&str, Person, VerboseError<&str>> {
    context("person",
        tuple((
            context("name", alpha1),
            context("separator", preceded(multispace0, tag(","))),
            context("age", preceded(multispace0, digit1))
        ))
    )(input)
    .map(|(remaining, (name, _, age))| {
        (remaining, Person {
            name: name.to_string(),
            age: age.parse().unwrap_or(0),
        })
    })
}
 
fn main() {
    let test_cases = [
        "John,25",      // Valid
        "John,  25",    // Valid with spaces
        "John",         // Missing separator and age
        "John,abc",     // Invalid age
        "123,25",       // Invalid name
    ];
    
    for input in test_cases {
        println!("\nParsing: '{}'", input);
        match parse_person(input) {
            Ok((remaining, person)) => {
                println!("  Success: {:?}", person);
                println!("  Remaining: '{}'", remaining);
            }
            Err(nom::Err::Error(e)) => {
                println!("  Error chain:");
                for (err_input, err_kind) in e.errors {
                    println!("    '{:.20}...' : {:?}",
                        err_input, err_kind);
                }
            }
            Err(nom::Err::Failure(e)) => {
                println!("  Fatal error:");
                for (err_input, err_kind) in e.errors {
                    println!("    '{:.20}...' : {:?}",
                        err_input, err_kind);
                }
            }
            _ => {}
        }
    }
}

Named contexts make it clear which parser component failed.

Error vs Failure in VerboseError

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::digit1,
    combinator::peek,
    error::{VerboseError, context, make_error},
    Err as NomErr,
};
 
fn parse_with_failure(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    // Error: recoverable, allows backtracking
    // Failure: unrecoverable, stops immediately
    
    // Use Err::Failure for errors that should not be backtracked past
    // This is useful when partial parsing has side effects
    
    context("parser", digit1)(input)
}
 
fn main() {
    // nom::Err::Error - recoverable, parser can try alternatives
    // nom::Err::Failure - unrecoverable, stop immediately
    
    let result = parse_with_failure("abc");
    match result {
        Err(NomErr::Error(e)) => {
            println!("Recoverable error:");
            println!("  Parser can try alternatives");
            for (input, kind) in e.errors {
                println!("  '{}' : {:?}", input, kind);
            }
        }
        Err(NomErr::Failure(e)) => {
            println!("Unrecoverable error:");
            println!("  Stop immediately");
            for (input, kind) in e.errors {
                println!("  '{}' : {:?}", input, kind);
            }
        }
        Ok(_) => println!("Success"),
        _ => {}
    }
}

Error allows backtracking; Failure stops the parser immediately.

Custom Error Types with VerboseError Support

use nom::{
    IResult,
    bytes::complete::tag,
    error::{VerboseError, context, ParseError, FromExternalError},
};
 
#[derive(Debug)]
enum CustomError {
    Nom(VerboseError<String>),
    Custom(String),
}
 
// Implement ParseError for custom type
impl<'a> ParseError<&'a str> for CustomError {
    fn from_error_kind(input: &'a str, kind: nom::error::ErrorKind) -> Self {
        CustomError::Nom(VerboseError::from_error_kind(input, kind))
    }
    
    fn append(input: &'a str, kind: nom::error::ErrorKind, other: Self) -> Self {
        match other {
            CustomError::Nom(mut e) => {
                e.errors.push((input, nom::error::VerboseErrorKind::Nom(kind)));
                CustomError::Nom(e)
            }
            CustomError::Custom(_) => other,
        }
    }
    
    fn from_char(input: &'a str, c: char) -> Self {
        Self::from_error_kind(input, nom::error::ErrorKind::Char)
    }
}
 
fn main() {
    println!("Custom error types can wrap VerboseError");
    println!("and add application-specific error information");
}

Custom error types can extend VerboseError with application-specific context.

Debugging Complex Parsers

use nom::{
    IResult,
    bytes::complete::{tag, take_while},
    character::complete::{alpha1, digit1, char},
    sequence::{tuple, delimited},
    branch::alt,
    multi::many0,
    error::{VerboseError, context},
};
 
#[derive(Debug)]
struct Config {
    name: String,
    version: String,
    items: Vec<String>,
}
 
fn parse_name(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("name_field",
        delimited(
            context("name_prefix", tag("name=")),
            context("name_value", alpha1),
            context("name_newline", char('\n'))
        )
    )(input)
}
 
fn parse_version(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("version_field",
        delimited(
            context("version_prefix", tag("version=")),
            context("version_value", take_while(|c: char| c.is_alphanumeric() || c == '.')),
            context("version_newline", char('\n'))
        )
    )(input)
}
 
fn parse_item(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("item",
        delimited(
            context("item_prefix", tag("- ")),
            context("item_value", alpha1),
            context("item_newline", char('\n'))
        )
    )(input)
}
 
fn parse_config(input: &str) -> IResult<&str, Config, VerboseError<&str>> {
    context("config",
        tuple((parse_name, parse_version, many0(parse_item)))
    )(input)
    .map(|(remaining, (name, version, items))| {
        (remaining, Config {
            name: name.to_string(),
            version: version.to_string(),
            items: items.iter().map(|s| s.to_string()).collect(),
        })
    })
}
 
fn main() {
    let bad_input = "name=Test\nversion=1.0\n- item1\n- item2\n- 123\n";
    // The last item has digits, which fails alpha1
    
    match parse_config(bad_input) {
        Ok((remaining, config)) => {
            println!("Success: {:?}", config);
        }
        Err(nom::Err::Error(e)) => {
            println!("Parsing failed:");
            for (input, kind) in e.errors {
                // Find which context failed
                println!("  Input: '{}'", input.lines().next().unwrap_or(""));
                println!("  Kind: {:?}", kind);
                println!();
            }
            // The error chain shows:
            // - item_value expected alpha, found '123'
            // - item context failed
            // - config context has error
        }
        _ => {}
    }
}

Detailed context at each level pinpoints exactly where complex parsers fail.

Error Chain Reconstruction

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::alpha1,
    sequence::preceded,
    error::{VerboseError, context},
};
 
fn parse_greeting(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("greeting",
        preceded(
            context("greeting_prefix", tag("Hello, ")),
            context("greeting_name", alpha1)
        )
    )(input)
}
 
fn describe_error(input: &str, error: VerboseError<&str>) -> String {
    use nom::error::VerboseErrorKind;
    
    let mut description = String::new();
    let original = input;
    
    for (error_input, kind) in error.errors {
        let offset = original.len() - error_input.len();
        let position = if offset < original.len() {
            format!("position {} ('{}')", offset, &original[offset..offset+1.min(original.len()-offset)])
        } else {
            format!("position {} (end)", offset)
        };
        
        match kind {
            VerboseErrorKind::Context(ctx) => {
                description.push_str(&format!(
                    "In '{}': expected input at {}\n",
                    ctx, position
                ));
            }
            VerboseErrorKind::Nom(err_kind) => {
                description.push_str(&format!(
                    "Failed {:?} at {}\n",
                    err_kind, position
                ));
            }
        }
    }
    
    description
}
 
fn main() {
    let input = "Hello, 123";
    match parse_greeting(input) {
        Err(nom::Err::Error(e)) => {
            println!("{}", describe_error(input, e));
        }
        Ok((remaining, matched)) => {
            println!("Matched: '{}', remaining: '{}'", matched, remaining);
        }
        _ => {}
    }
}

Reconstructing the error chain produces human-readable error messages.

Performance Considerations

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::digit1,
    error::{Error, VerboseError},
};
 
fn parse_with_error(input: &str) -> IResult<&str, &str, Error<&str>> {
    // Error type: minimal overhead, no allocation
    // Good for production where errors are handled simply
    tag("prefix")(input)
}
 
fn parse_with_verbose(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    // VerboseError: allocates Vec for error chain
    // More overhead but better error messages
    // Good for development and user-facing errors
    tag("prefix")(input)
}
 
fn main() {
    let input = "prefix123";
    
    // Production: use Error for speed
    let _: IResult<&str, &str, Error<&str>> = parse_with_error(input);
    
    // Development: use VerboseError for debugging
    let _: IResult<&str, &str, VerboseError<&str>> = parse_with_verbose(input);
    
    println!("Error<T>: Minimal overhead, simple errors");
    println!("VerboseError<T>: More overhead, rich context");
}

VerboseError has allocation overhead; use Error for production when detailed messages aren't needed.

Converting Between Error Types

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::digit1,
    error::{Error, VerboseError, context},
};
 
fn parse_simple(input: &str) -> IResult<&str, &str, Error<&str>> {
    tag("hello")(input)
}
 
fn parse_verbose(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    context("greeting", tag("hello"))(input)
}
 
fn main() {
    // Convert from simple to verbose if needed
    let result = parse_simple("world");
    match result {
        Err(nom::Err::Error(e)) => {
            // Can convert Error to VerboseError if needed
            let verbose = VerboseError::from_error_kind(e.input, e.code);
            println!("Converted to verbose: {:?}", verbose);
        }
        _ => {}
    }
    
    // Or use VerboseError from the start for development
    let result = parse_verbose("world");
    match result {
        Err(nom::Err::Error(e)) => {
            println!("Already verbose: {:?}", e);
        }
        _ => {}
    }
}

Start with VerboseError for development; consider Error for production if needed.

Practical Pattern: Parser Error Reporting

use nom::{
    IResult,
    bytes::complete::tag,
    character::complete::{alpha1, digit1, multispace0},
    sequence::{tuple, preceded},
    error::{VerboseError, context},
};
 
fn parse_assignment(input: &str) -> IResult<&str, (&str, &str), VerboseError<&str>> {
    context("assignment",
        tuple((
            context("identifier", alpha1),
            context("whitespace1", multispace0),
            context("equals", tag("=")),
            context("whitespace2", multispace0),
            context("value", digit1)
        ))
    )(input)
    .map(|(remaining, (id, _, _, _, val))| {
        (remaining, (id, val))
    })
}
 
fn report_error(input: &str, error: VerboseError<&str>) -> String {
    use nom::error::VerboseErrorKind;
    
    let mut lines = input.lines().enumerate().collect::<Vec<_>>();
    let mut report = String::new();
    
    for (error_input, kind) in error.errors.iter().rev() {
        let offset = input.len() - error_input.len();
        
        // Find line and column
        let mut current_offset = 0;
        let mut line_num = 1;
        let mut col = 1;
        
        for (i, c) in input.char_indices() {
            if i >= offset {
                break;
            }
            if c == '\n' {
                line_num += 1;
                col = 1;
            } else {
                col += 1;
            }
        }
        
        let snippet = input.lines().nth(line_num - 1).unwrap_or("");
        
        match kind {
            VerboseErrorKind::Context(ctx) => {
                report.push_str(&format!(
                    "at line {} column {} in context '{}'\n",
                    line_num, col, ctx
                ));
                report.push_str(&format!("  {}\n", snippet));
                report.push_str(&format!("  {}^\n", " ".repeat(col - 1)));
            }
            VerboseErrorKind::Nom(err_kind) => {
                report.push_str(&format!(
                    "at line {} column {}: {:?}\n",
                    line_num, col, err_kind
                ));
            }
        }
    }
    
    report
}
 
fn main() {
    let input = "x = abc";
    
    match parse_assignment(input) {
        Ok((remaining, (id, val))) => {
            println!("Parsed: {} = {}", id, val);
        }
        Err(nom::Err::Error(e)) => {
            println!("{}", report_error(input, e));
        }
        _ => {}
    }
}

Combining VerboseError with line/column calculation produces user-friendly error reports.

Synthesis

VerboseError components:

Component Purpose
errors: Vec<(I, VerboseErrorKind)> Chain of failure points
VerboseErrorKind::Context(&str) Named context from context()
VerboseErrorKind::Nom(ErrorKind) Base nom error types

Error chain accumulation:

Combinator Effect on Error Chain
context(name, parser) Adds named context on failure
alt(p1, p2, ...) Accumulates errors from all alternatives
tuple(p1, p2, ...) Shows failure at first failing parser
many0, many1 Context from failing element

Error types in nom:

Type Use Case Overhead
(I, ErrorKind) Minimal, production None
Error<I> Simple error handling Small
VerboseError<I> Development, debugging Vec allocation
Custom types Application-specific Varies

Key insight: VerboseError transforms parser debugging from "something failed somewhere" to "the digit1 parser in the age context failed at position 42 expecting a digit but found 'x'." This information is invaluable during parser development, where combinators nest deeply and failures can occur at any level. The error chain records the complete path from the outermost parser down to the failing leaf, accumulating (input, error_kind) pairs at each context() wrapper and each failure point. When an alt combinator tries multiple alternatives, VerboseError collects errors from all branches, showing why each one failed. The trade-off is allocation—VerboseError builds a Vec of error pairs, while the base error type carries no allocation. For development and user-facing error reporting, this overhead is negligible; for hot paths in production with simple error handling, consider the base error type. The context() combinator is the key tool: wrap parsers at meaningful boundaries with context("parser_name", parser) to annotate where failures occur. Without context, error chains show only Nom(ErrorKind) values; with context, each level is labeled, making it clear whether the failure was in name, separator, or age rather than just "expected digit at offset 5."