How does nom::combinator::map_res enable error type transformation in parser combinators?

The nom::combinator::map_res combinator bridges the gap between raw parsing results and application-specific error types. It applies a transformation function to a successful parse result, but crucially, allows that function to return a Result—if the function returns Err, the entire parser fails with that error. This enables enriching parse errors with context, validating parsed values during parsing, converting between error types, and implementing domain-specific validation that integrates cleanly with nom's error handling infrastructure. Unlike simple map, which only transforms successful values, map_res can fail the parser based on the transformation's outcome.

Basic map_res Usage

use nom::{IResult, combinator::map_res, bytes::complete::tag};
 
fn parse_digit(input: &str) -> IResult<&str, u8> {
    // map_res applies a function that returns Result
    map_res(
        tag("42"),
        |s: &str| s.parse::<u8>()
    )(input)
}
 
#[test]
fn test_digit() {
    assert_eq!(parse_digit("42abc"), Ok(("abc", 42)));
    // If parse fails, the parser fails
    assert!(parse_digit("43abc").is_err());
}

map_res takes a parser and a fallible function, applying the function to successful parse results.

Error Type Transformation

use nom::{IResult, error::{Error, ErrorKind}, combinator::map_res, bytes::complete::take_while};
 
#[derive(Debug)]
pub enum AppError {
    ParseError(String),
    InvalidValue(String),
}
 
fn parse_number(input: &str) -> IResult<&str, i32, AppError> {
    map_res(
        take_while(|c: char| c.is_ascii_digit()),
        |digits: &str| {
            digits.parse::<i32>().map_err(|e| {
                AppError::InvalidValue(format!("Failed to parse '{}': {}", digits, e))
            })
        }
    )(input)
}
 
// The error type transforms from nom's default to AppError

When the function returns Err, that error becomes the parser's error.

Validation During Parsing

use nom::{IResult, combinator::map_res, bytes::complete::take_while1};
 
fn parse_age(input: &str) -> IResult<&str, u8, String> {
    map_res(
        take_while1(|c: char| c.is_ascii_digit()),
        |digits: &str| {
            let age: u8 = digits.parse().map_err(|_| "Invalid number".to_string())?;
            if age > 150 {
                Err(format!("Age {} is unreasonably high", age))
            } else if age == 0 {
                Err("Age cannot be zero".to_string())
            } else {
                Ok(age)
            }
        }
    )(input)
}
 
#[test]
fn test_age_validation() {
    assert_eq!(parse_age("25"), Ok(("", 25)));
    assert!(parse_age("200").is_err()); // Too high
    assert!(parse_age("0").is_err()); // Zero not allowed
}

Validation logic can fail the parser with meaningful error messages.

Adding Context to Errors

use nom::{IResult, combinator::map_res, character::complete::digit1};
 
#[derive(Debug)]
pub struct ContextualError {
    pub message: String,
    pub input: String,
    pub context: String,
}
 
fn parse_port(input: &str) -> IResult<&str, u16, ContextualError> {
    map_res(
        digit1,
        |digits: &str| {
            let port: u16 = digits.parse().map_err(|_| ContextualError {
                message: "Not a valid port number".to_string(),
                input: digits.to_string(),
                context: "parsing port".to_string(),
            })?;
            
            if port == 0 {
                Err(ContextualError {
                    message: "Port cannot be zero".to_string(),
                    input: digits.to_string(),
                    context: "validating port range".to_string(),
                })
            } else {
                Ok(port)
            }
        }
    )(input)
}

map_res enables rich error types with context beyond what nom provides by default.

Chaining Validations

use nom::{IResult, combinator::map_res, bytes::complete::take_while, sequence::tuple};
 
fn parse_username(input: &str) -> IResult<&str, String, String> {
    map_res(
        take_while(|c: char| c.is_alphanumeric() || c == '_'),
        |s: &str| {
            if s.len() < 3 {
                Err(format!("Username '{}' too short (minimum 3 characters)", s))
            } else if s.len() > 20 {
                Err(format!("Username '{}' too long (maximum 20 characters)", s))
            } else if s.starts_with('_') {
                Err(format!("Username '{}' cannot start with underscore", s))
            } else {
                Ok(s.to_string())
            }
        }
    )(input)
}
 
fn parse_user(input: &str) -> IResult<&str, (String, u8), String> {
    let (remaining, (name, _, age)) = tuple((
        parse_username,
        nom::character::complete::space1,
        map_res(
            nom::character::complete::digit1,
            |digits: &str| {
                let age: u8 = digits.parse().map_err(|_| "Invalid age".to_string())?;
                if age < 13 {
                    Err(format!("User must be at least 13 years old, got {}", age))
                } else if age > 120 {
                    Err(format!("Age {} is unrealistic", age))
                } else {
                    Ok(age)
                }
            }
        )
    ))(input)?;
    
    Ok((remaining, (name, age)))
}

Multiple validations compose naturally through map_res.

Converting Between Error Types

use nom::{IResult, error::Error, combinator::map_res, bytes::complete::tag};
 
// Define application error types
#[derive(Debug)]
pub enum ParseError {
    NomError(nom::error::ErrorKind),
    InvalidSyntax(String),
    OutOfRange { value: i64, min: i64, max: i64 },
}
 
fn parse_bounded(input: &str) -> IResult<&str, i32, ParseError> {
    map_res(
        nom::character::complete::i64,
        |value: i64| {
            const MIN: i64 = -100;
            const MAX: i64 = 100;
            
            if value < MIN {
                Err(ParseError::OutOfRange { value, min: MIN, max: MAX })
            } else if value > MAX {
                Err(ParseError::OutOfRange { value, min: MIN, max: MAX })
            } else {
                Ok(value as i32)
            }
        }
    )(input)
}
 
// Converts i64 parse errors to application error type

map_res translates between nom's internal errors and application-specific types.

Working with Custom Error Types

use nom::{IResult, combinator::map_res, character::complete::{digit1, space0}};
 
#[derive(Debug, Clone)]
pub struct JsonPathError {
    pub position: usize,
    pub expected: String,
    pub found: String,
}
 
fn parse_index(input: &str) -> IResult<&str, usize, JsonPathError> {
    let position = input.len();
    
    map_res(
        digit1,
        |digits: &str| {
            digits.parse::<usize>().map_err(|_| JsonPathError {
                position,
                expected: "array index".to_string(),
                found: digits.to_string(),
            })
        }
    )(input)
}
 
fn parse_array_access(input: &str) -> IResult<&str, Vec<usize>, JsonPathError> {
    let mut indices = Vec::new();
    let mut remaining = input;
    
    loop {
        let (rem, _) = space0(remaining)?;
        let (rem, _) = nom::bytes::complete::tag("[")(rem)?;
        let (rem, idx) = parse_index(rem)?;
        let (rem, _) = nom::bytes::complete::tag("]")(rem)?;
        
        indices.push(idx);
        remaining = rem;
        
        if !remaining.starts_with("[") {
            break;
        }
    }
    
    Ok((remaining, indices))
}

Custom error types enable domain-specific error handling throughout the parser.

Comparison with map

use nom::{IResult, combinator::{map, map_res}};
 
// map: Always succeeds, transforms value
fn parse_with_map(input: &str) -> IResult<&str, String> {
    map(
        nom::character::complete::alpha1,
        |s: &str| s.to_uppercase()
    )(input)
}
 
// map_res: Can fail, transforms value with validation
fn parse_with_map_res(input: &str) -> IResult<&str, String, String> {
    map_res(
        nom::character::complete::alpha1,
        |s: &str| {
            if s.len() > 10 {
                Err("String too long".to_string())
            } else {
                Ok(s.to_uppercase())
            }
        }
    )(input)
}
 
#[test]
fn test_comparison() {
    // map always succeeds with transformation
    assert!(parse_with_map("averylongstring").is_ok());
    
    // map_res can fail
    assert!(parse_with_map_res("averylongstring").is_err());
    assert!(parse_with_map_res("short").is_ok());
}

Use map when transformation always succeeds; use map_res when it might fail.

Error Recovery Patterns

use nom::{IResult, combinator::{map_res, alt}, character::complete::digit1};
 
#[derive(Debug)]
pub enum FieldError {
    InvalidFormat(String),
    ValueTooLarge { max: usize },
}
 
fn parse_optional_size(input: &str) -> IResult<&str, Option<usize>, FieldError> {
    alt((
        // "unlimited" maps to None
        map_res(
            nom::bytes::complete::tag("unlimited"),
            |_| Ok::<_, FieldError>(None)
        ),
        // Number maps to Some(size)
        map_res(
            digit1,
            |digits: &str| {
                let size: usize = digits.parse().map_err(|_| {
                    FieldError::InvalidFormat(digits.to_string())
                })?;
                
                if size > 1_000_000 {
                    Err(FieldError::ValueTooLarge { max: 1_000_000 })
                } else {
                    Ok(Some(size))
                }
            }
        )
    ))(input)
}

map_res works within alt to try multiple parsing strategies with validation.

Real-World Example: Parsing Configuration Values

use nom::{IResult, combinator::map_res, bytes::complete::take_while1, character::complete::char};
 
#[derive(Debug, Clone)]
pub enum ConfigValue {
    Integer(i64),
    Float(f64),
    Boolean(bool),
    String(String),
}
 
#[derive(Debug)]
pub struct ConfigParseError {
    pub field: String,
    pub reason: String,
}
 
fn parse_config_value(input: &str) -> IResult<&str, ConfigValue, ConfigParseError> {
    // Try boolean first
    let bool_result = map_res(
        nom::bytes::complete::alt((
            nom::bytes::complete::tag("true"),
            nom::bytes::complete::tag("false"),
        )),
        |s: &str| {
            match s {
                "true" => Ok(ConfigValue::Boolean(true)),
                "false" => Ok(ConfigValue::Boolean(false)),
                _ => Err(ConfigParseError {
                    field: s.to_string(),
                    reason: "Invalid boolean".to_string(),
                }),
            }
        }
    )(input);
    
    if bool_result.is_ok() {
        return bool_result;
    }
    
    // Try integer
    let int_result = map_res(
        nom::character::complete::i64,
        |n| Ok::<_, ConfigParseError>(ConfigValue::Integer(n))
    )(input);
    
    if int_result.is_ok() {
        return int_result;
    }
    
    // Fall back to string
    map_res(
        take_while1(|_| true),
        |s: &str| Ok::<_, ConfigParseError>(ConfigValue::String(s.to_string()))
    )(input)
}
 
fn parse_config_line(input: &str) -> IResult<&str, (String, ConfigValue), ConfigParseError> {
    let (remaining, key) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
        .map_err(|_| ConfigParseError {
            field: "".to_string(),
            reason: "Expected key".to_string(),
        })?;
    
    let (remaining, _) = char('=').parse(remaining)
        .map_err(|_| ConfigParseError {
            field: key.to_string(),
            reason: "Expected '=' after key".to_string(),
        })?;
    
    let (remaining, value) = parse_config_value(remaining)?;
    
    Ok((remaining, (key.to_string(), value)))
}

Configuration parsing often needs validation that integrates with nom's error flow.

Real-World Example: Parsing Network Addresses

use nom::{IResult, combinator::map_res, sequence::separated_pair, character::complete::digit1};
 
#[derive(Debug)]
pub struct AddrError {
    pub message: String,
}
 
fn parse_u8(input: &str) -> IResult<&str, u8, AddrError> {
    map_res(
        digit1,
        |digits: &str| {
            digits.parse::<u8>().map_err(|_| AddrError {
                message: format!("'{}' is not a valid u8", digits),
            })
        }
    )(input)
}
 
fn parse_ipv4_octet(input: &str) -> IResult<&str, u8, AddrError> {
    map_res(
        parse_u8,
        |value| {
            if value > 255 {
                Err(AddrError {
                    message: format!("{} exceeds IPv4 octet maximum (255)", value),
                })
            } else {
                Ok(value)
            }
        }
    )(input)
}
 
fn parse_ipv4(input: &str) -> IResult<&str, [u8; 4], AddrError> {
    let (remaining, (a, _, b, _, c, _, d)) = nom::sequence::tuple((
        parse_ipv4_octet,
        nom::character::complete::char('.'),
        parse_ipv4_octet,
        nom::character::complete::char('.'),
        parse_ipv4_octet,
        nom::character::complete::char('.'),
        parse_ipv4_octet,
    ))(input)?;
    
    Ok((remaining, [a, b, c, d]))
}
 
fn parse_port(input: &str) -> IResult<&str, u16, AddrError> {
    map_res(
        digit1,
        |digits: &str| {
            let port: u16 = digits.parse().map_err(|_| AddrError {
                message: format!("'{}' is not a valid port", digits),
            })?;
            
            if port == 0 {
                Err(AddrError {
                    message: "Port 0 is not allowed".to_string(),
                })
            } else {
                Ok(port)
            }
        }
    )(input)
}
 
fn parse_socket_addr(input: &str) -> IResult<&str, ([u8; 4], u16), AddrError> {
    let (remaining, (ip, _, port)) = nom::sequence::tuple((
        parse_ipv4,
        nom::character::complete::char(':'),
        parse_port,
    ))(input)?;
    
    Ok((remaining, (ip, port)))
}

Network address parsing requires validation at multiple levels.

Error Message Formatting

use nom::{IResult, combinator::map_res, character::complete::{space0, space1}};
 
#[derive(Debug)]
pub struct FormattedError {
    pub message: String,
    pub location: Option<String>,
}
 
impl std::fmt::Display for FormattedError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match &self.location {
            Some(loc) => write!(f, "{} at {}", self.message, loc),
            None => write!(f, "{}", self.message),
        }
    }
}
 
fn parse_token(name: &'static str) -> impl Fn(&str) -> IResult<&str, &str, FormattedError> {
    move |input: &str| {
        let (remaining, token) = nom::bytes::complete::is_a("abcdefghijklmnopqrstuvwxyz")(input)
            .map_err(|_| FormattedError {
                message: format!("Expected {}", name),
                location: None,
            })?;
        
        map_res(
            nom::combinator::success(token),
            |t: &str| {
                if t.len() < 2 {
                    Err(FormattedError {
                        message: format!("{} too short", name),
                        location: Some(format!("position {}", input.len() - remaining.len())),
                    })
                } else {
                    Ok(t)
                }
            }
        )(remaining)
    }
}

map_res enables precise error locations and formatted messages.

Combining with Other Error Combinators

use nom::{IResult, combinator::{map_res, map_parser}, character::complete::digit1};
 
#[derive(Debug)]
pub enum ParserError {
    InvalidDigit(String),
    OutOfRange(i64),
    Custom(String),
}
 
fn parse_hex_digit(input: &str) -> IResult<&str, u8, ParserError> {
    map_res(
        nom::bytes::complete::take_while_m_n(1, 2, |c: char| c.is_ascii_hexdigit()),
        |hex: &str| {
            u8::from_str_radix(hex, 16).map_err(|_| {
                ParserError::InvalidDigit(hex.to_string())
            })
        }
    )(input)
}
 
fn parse_validated_number(input: &str) -> IResult<&str, i64, ParserError> {
    map_res(
        digit1,
        |digits: &str| {
            let value: i64 = digits.parse().map_err(|_| {
                ParserError::Custom(format!("'{}' is not a valid number", digits))
            })?;
            
            if value < 0 {
                Err(ParserError::OutOfRange(value))
            } else if value > 1000 {
                Err(ParserError::OutOfRange(value))
            } else {
                Ok(value)
            }
        }
    )(input)
}

map_res integrates with other combinators for complex parsing flows.

Synthesis

Key behaviors:

Behavior Description
Applies function Transforms successful parse results
Can fail Returns Err if function returns Err
Error transformation Converts function errors to parser errors
Validation Enables in-parser value validation

When to use:

Use case Example
String to number map_res(digit1, |s| s.parse::<i32>())
Range validation Check bounds after parsing
Custom errors Convert to domain error types
Multi-step validation Parse then validate

Comparison with related combinators:

Combinator Can fail? Transforms error?
map No N/A
map_res Yes Yes (from function)
map_parser Yes No
verify Yes No

Key insight: nom::combinator::map_res is the primary bridge between nom's parsing infrastructure and application-specific validation and error handling. It takes a parser and a fallible transformation function, applying the function to successful parse results. If the function returns Err, the parser fails with that error, enabling validation failures to propagate through nom's error handling system. This is essential for domain-specific validation (like range checks on parsed numbers), converting between error types (from nom's generic errors to application-specific types), adding context to errors (field names, positions, expectations), and implementing multi-stage parsing where later stages validate earlier results. Unlike map, which only transforms successful values, map_res can fail the parser based on validation logic, making it the key combinator for integrating business rules into the parsing process itself.