Error Handling
Valid8r provides structured error handling through the ValidationError dataclass and ErrorCode constants. This enables programmatic error handling, machine-readable error responses, and better debugging for validation failures.
Why Structured Errors?
Traditional error handling with plain strings makes it difficult to:
Handle errors programmatically: You can’t easily distinguish between different error types
Build API responses: Converting errors to JSON requires manual parsing
Track error locations: No standard way to identify which field failed
Provide debugging context: Limited ability to include validation parameters
Valid8r’s structured errors solve these problems while maintaining backward compatibility with string-based errors.
ValidationError Dataclass
The ValidationError dataclass provides a comprehensive error representation:
from valid8r.core.errors import ValidationError
error = ValidationError(
code='INVALID_EMAIL',
message='Email address format is invalid',
path='.user.email',
context={'input': 'not-an-email'}
)
Attributes:
code(str): Machine-readable error code (e.g.,'INVALID_EMAIL','OUT_OF_RANGE')message(str): Human-readable error message describing the failurepath(str): JSON path to the failed field (e.g.,'.user.email','.items[0].name')context(dict | None): Additional debugging information (e.g.,{'min': 0, 'max': 100, 'value': 150})
Methods:
__str__(): Returns human-readable format ('path: message'or just'message')to_dict(): Converts error to dictionary for JSON serialization
ErrorCode Constants
The ErrorCode class provides standard error codes organized by category:
from valid8r.core.errors import ErrorCode
# Use predefined constants
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email address format is invalid'
)
Available Error Codes:
Parsing Errors:
INVALID_TYPE- Type conversion failed (e.g., string to int)INVALID_FORMAT- Input format does not match expected patternPARSE_ERROR- General parsing failure
Numeric Validators:
OUT_OF_RANGE- Value is outside the allowed rangeBELOW_MINIMUM- Value is below the minimum allowed valueABOVE_MAXIMUM- Value is above the maximum allowed value
String Validators:
TOO_SHORT- String length is below minimumTOO_LONG- String length exceeds maximumPATTERN_MISMATCH- String does not match required regex patternEMPTY_STRING- String is empty when a value is required
Collection Validators:
NOT_IN_SET- Value is not in the allowed setDUPLICATE_ITEMS- Collection contains duplicate itemsINVALID_SUBSET- Collection is not a valid subset
Network Validators:
INVALID_EMAIL- Email address format is invalidINVALID_URL- URL format is invalidINVALID_IP- IP address format is invalidINVALID_PHONE- Phone number format is invalid
Filesystem Validators:
PATH_NOT_FOUND- File or directory path does not existNOT_A_FILE- Path exists but is not a fileNOT_A_DIRECTORY- Path exists but is not a directoryFILE_TOO_LARGE- File size exceeds maximum allowed size
DoS Protection:
INPUT_TOO_LONG- Input exceeds maximum length (DoS protection)
Generic:
CUSTOM_ERROR- User-defined custom validation errorVALIDATION_ERROR- Generic validation failure
Creating Failure with ValidationError
The Failure type now accepts both string errors (backward compatible) and ValidationError instances:
from valid8r import Maybe
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ValidationError, ErrorCode
# Old way (still works): plain string error
failure = Maybe.failure('Email address format is invalid')
# New way: structured error with code
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email address format is invalid',
path='.user.email',
context={'input': 'not-an-email'}
)
failure = Maybe.failure(error)
# Access the structured error
match failure:
case Failure(error_msg):
# Pattern matching still works with message string
print(error_msg) # 'Email address format is invalid'
Accessing Structured Error Information
Use the error_detail() method or validation_error property to access the full structured error:
from valid8r import Maybe
from valid8r.core.maybe import Failure
from valid8r.core.errors import ValidationError, ErrorCode
error = ValidationError(
code=ErrorCode.OUT_OF_RANGE,
message='Value must be between 0 and 100',
path='.user.age',
context={'value': 150, 'min': 0, 'max': 100}
)
failure = Maybe.failure(error)
# Access structured error details using error_detail() (RFC-001 Phase 2)
match failure:
case Failure():
detail = failure.error_detail()
print(f"Code: {detail.code}") # Code: OUT_OF_RANGE
print(f"Message: {detail.message}") # Message: Value must be between 0 and 100
print(f"Path: {detail.path}") # Path: .user.age
print(f"Context: {detail.context}") # Context: {'value': 150, 'min': 0, 'max': 100}
# Alternative: use validation_error property (also available)
match failure:
case Failure():
ve = failure.validation_error
print(f"Code: {ve.code}") # Code: OUT_OF_RANGE
print(f"Message: {ve.message}") # Message: Value must be between 0 and 100
print(f"Path: {ve.path}") # Path: .user.age
print(f"Context: {ve.context}") # Context: {'value': 150, 'min': 0, 'max': 100}
Programmatic Error Handling
Use error codes to handle different validation failures programmatically:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ErrorCode
def process_email(email_str: str):
result = parsers.parse_email(email_str)
match result:
case Success(email):
return f"Valid email: {email.local}@{email.domain}"
case Failure():
# Access structured error for programmatic handling
error = result.error_detail()
# Different handling based on error code
match error.code:
case ErrorCode.INVALID_EMAIL:
return "Please enter a valid email address"
case ErrorCode.EMPTY_STRING:
return "Email is required"
case ErrorCode.INPUT_TOO_LONG:
return "Email is too long"
case _:
return f"Validation error: {error.message}"
print(process_email("user@example.com")) # Valid email: user@example.com
print(process_email("not-an-email")) # Please enter a valid email address
print(process_email("")) # Email is required
Converting Errors to JSON
Use the to_dict() method to serialize errors for API responses:
from valid8r import parsers
from valid8r.core.maybe import Failure
import json
def validate_api_request(data: dict) -> dict:
"""Validate API request and return JSON-serializable response."""
age_result = parsers.parse_int(data.get('age', ''))
match age_result:
case Failure():
error_dict = age_result.error_detail().to_dict()
return {
'status': 'error',
'error': error_dict
}
case _:
return {'status': 'success'}
# Example with invalid data
response = validate_api_request({'age': 'not-a-number'})
print(json.dumps(response, indent=2))
# {
# "status": "error",
# "error": {
# "code": "INVALID_TYPE",
# "message": "Input must be a valid integer",
# "path": "",
# "context": {}
# }
# }
Pattern Matching with Structured Errors
Combine pattern matching with structured error codes for elegant error handling:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ErrorCode
def validate_age(age_str: str) -> str:
result = parsers.parse_int(age_str).bind(
validators.minimum(0) & validators.maximum(120)
)
match result:
case Success(age) if age >= 18:
return f"Adult: {age} years old"
case Success(age):
return f"Minor: {age} years old"
case Failure() if result.error_detail().code == ErrorCode.INVALID_TYPE:
return "Please enter a number"
case Failure() if result.error_detail().code == ErrorCode.BELOW_MINIMUM:
return "Age cannot be negative"
case Failure() if result.error_detail().code == ErrorCode.ABOVE_MAXIMUM:
return "Age seems unrealistic"
case Failure(error):
return f"Validation failed: {error}"
print(validate_age("25")) # Adult: 25 years old
print(validate_age("10")) # Minor: 10 years old
print(validate_age("abc")) # Please enter a number
print(validate_age("-5")) # Age cannot be negative
print(validate_age("150")) # Age seems unrealistic
Multi-Field Validation with Paths
Use the path attribute to track which field failed in complex validations:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ValidationError, ErrorCode
def validate_user_data(data: dict) -> list[dict]:
"""Validate user data and return list of errors."""
errors = []
# Validate name
name = data.get('name', '')
if not name:
errors.append(ValidationError(
code=ErrorCode.EMPTY_STRING,
message='Name is required',
path='.name'
).to_dict())
# Validate email
email_result = parsers.parse_email(data.get('email', ''))
match email_result:
case Failure():
error = email_result.error_detail()
errors.append({
**error.to_dict(),
'path': '.email' # Set field path
})
# Validate age
age_result = parsers.parse_int(data.get('age', '')).bind(
validators.minimum(0) & validators.maximum(120)
)
match age_result:
case Failure():
error = age_result.error_detail()
errors.append({
**error.to_dict(),
'path': '.age' # Set field path
})
return errors
# Example with invalid data
invalid_data = {
'name': '',
'email': 'not-an-email',
'age': 'abc'
}
errors = validate_user_data(invalid_data)
for error in errors:
print(f"{error['path']}: {error['message']}")
# .name: Name is required
# .email: Email address format is invalid
# .age: Input must be a valid integer
Backward Compatibility
Structured errors maintain complete backward compatibility with string-based errors:
from valid8r import Maybe
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ValidationError, ErrorCode
# Old code using string errors still works
failure = Maybe.failure('Something went wrong')
match failure:
case Failure(error):
print(error) # 'Something went wrong'
# String errors are automatically wrapped in ValidationError
match failure:
case Failure():
detail = failure.error_detail()
print(detail.code) # 'VALIDATION_ERROR'
print(detail.message) # 'Something went wrong'
# New code can use structured errors
structured_failure = Maybe.failure(
ValidationError(code=ErrorCode.INVALID_EMAIL, message='Bad email')
)
# Pattern matching still works the same way
match structured_failure:
case Failure(error):
print(error) # 'Bad email'
# Access structured details from any Failure
match structured_failure:
case Failure():
detail = structured_failure.error_detail()
print(detail.code) # 'INVALID_EMAIL'
print(detail.message) # 'Bad email'
Migration Guide
Migrating existing code to use structured errors is optional and can be done incrementally:
Step 1: Continue using string errors (no changes needed)
Your existing code continues to work without any modifications:
# Existing code still works
result = Maybe.failure('Email format is invalid')
Step 2: Add structured errors to new code
Start using ValidationError in new parsers and validators:
from valid8r.core.errors import ValidationError, ErrorCode
# New code with structured errors
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email format is invalid'
)
result = Maybe.failure(error)
Step 3: Use programmatic error handling where needed
Take advantage of error codes in specific places where you need programmatic handling using error_detail():
match result:
case Failure() if result.error_detail().code == ErrorCode.INVALID_EMAIL:
# Handle email errors specifically
send_email_format_help()
case Failure(error):
# Generic error handling
log_error(error)
Step 4: Convert errors to JSON for APIs
Use error_detail().to_dict() for API responses without changing your validation logic:
match result:
case Failure():
return {
'status': 'error',
'error': result.error_detail().to_dict()
}
Best Practices
Use ErrorCode Constants
Always use ErrorCode constants instead of hardcoded strings:
# Good: Use constants
error = ValidationError(code=ErrorCode.INVALID_EMAIL, message='Bad email')
# Bad: Hardcoded string
error = ValidationError(code='INVALID_EMAIL', message='Bad email')
Provide Context for Debugging
Include validation parameters in the context for better debugging:
# Good: Include context
error = ValidationError(
code=ErrorCode.OUT_OF_RANGE,
message='Value must be between 0 and 100',
context={'value': 150, 'min': 0, 'max': 100}
)
# OK: No context (when not needed)
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email format is invalid'
)
Set Paths for Multi-Field Validation
Always set the path attribute when validating multiple fields:
# Good: Set path
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email format is invalid',
path='.user.email'
)
# OK: No path for single-field validation
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Email format is invalid'
)
Keep Error Messages User-Friendly
Write messages for end users, not developers:
# Good: User-friendly message
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Please enter a valid email address'
)
# Bad: Technical jargon
error = ValidationError(
code=ErrorCode.INVALID_EMAIL,
message='Regex pattern match failed for email validation'
)
Next Steps
Now that you understand structured error handling, you can:
Use parsers that return structured errors
Build custom validators with error codes
Create robust API integrations with JSON error responses
See practical examples in the examples section