Validation Error Handling
In this tutorial, you'll learn about Validation Error Handling. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Validation error handling returns structured, consistent error responses when validation fails, helping clients understand what went wrong and how to fix it.
What You'll Learn
By the end of this lesson, you will implement structured error responses, aggregate field-level errors, add error codes, and handle nested validation errors.
Why It Matters
Poor error handling forces clients to guess what went wrong. Good error responses pinpoints the exact issue, reducing support requests and debug time.
Real-World Use
Stripe's API returns field-level errors: {"error": {"code": "validation_error", "param": "email", "message": "Invalid email address"}}. Clients use the param field to highlight the problematic input.
Error Response Flow
flowchart LR
Validate[Validation Fails] --> Collect[Collect All Errors]
Collect --> Format[Format Error Response]
Format --> Status[400 Bad Request]
Status --> Client[Client Fixes Errors]
Client --> Retry[Retry Request]
Structured Error Response
# error_response.py
from typing import Any, Dict, List, Optional
class ValidationError:
def __init__(self, field: str, message: str, code: str = "invalid",
value: Any = None):
self.field = field
self.message = message
self.code = code
self.value = value
def to_dict(self) -> Dict:
result = {
"field": self.field,
"message": self.message,
"code": self.code,
}
if self.value is not None:
result["value"] = self.value
return result
class ErrorResponse:
def __init__(self):
self.errors: List[ValidationError] = []
def add_error(self, field: str, message: str, code: str = "invalid",
value: Any = None):
self.errors.append(ValidationError(field, message, code, value))
def add_required(self, field: str):
self.add_error(field, f"{field} is required", "required")
def add_type_error(self, field: str, expected: str):
self.add_error(field, f"{field} must be {expected}", "type_error")
def to_response(self) -> Dict:
return {
"status": 400,
"error": "Validation failed",
"details": [e.to_dict() for e in self.errors],
}
def ok(self) -> bool:
return len(self.errors) == 0
resp = ErrorResponse()
resp.add_required("email")
resp.add_type_error("age", "integer")
resp.add_error("password", "Password must be at least 8 characters", "min_length")
resp2 = ErrorResponse()
print(f"With errors: resp.ok={resp.ok()}")
for err in resp.errors:
print(f" {err.to_dict()}")
print(f"No errors: resp2.ok={resp2.ok()}")
Expected output:
With errors: resp.ok=False
{'field': 'email', 'message': 'email is required', 'code': 'required'}
{'field': 'age', 'message': 'age must be integer', 'code': 'type_error'}
{'field': 'password', 'message': 'Password must be at least 8 characters', 'code': 'min_length'}
No errors: resp2.ok=True
Field-Level Error Formatter
# field_errors.py
from typing import Any, Dict, List
class FieldErrorFormatter:
def __init__(self):
self.errors: Dict[str, List[str]] = {}
def add(self, field: str, message: str):
if field not in self.errors:
self.errors[field] = []
self.errors[field].append(message)
def to_flat(self) -> Dict:
return {
"errors": [
{"field": f, "messages": msgs}
for f, msgs in self.errors.items()
]
}
def to_nested(self) -> Dict:
return {
"errors": {
field: {"messages": msgs}
for field, msgs in self.errors.items()
}
}
formatter = FieldErrorFormatter()
formatter.add("email", "Required")
formatter.add("email", "Invalid format")
formatter.add("password", "Too short")
print("Flat:")
print(formatter.to_flat())
print("\nNested:")
print(formatter.to_nested())
Expected output:
Flat:
{'errors': [{'field': 'email', 'messages': ['Required', 'Invalid format']}, {'field': 'password', 'messages': ['Too short']}]}
Nested:
{'errors': {'email': {'messages': ['Required', 'Invalid format']}, 'password': {'messages': ['Too short']}}}
Error Code Standards
# error_codes.py
from typing import Any, Dict, List, Optional
class ErrorCode:
REQUIRED = "required"
INVALID_FORMAT = "invalid_format"
TYPE_ERROR = "type_error"
MIN_LENGTH = "min_length"
MAX_LENGTH = "max_length"
MIN_VALUE = "min_value"
MAX_VALUE = "max_value"
NOT_UNIQUE = "not_unique"
PATTERN_MISMATCH = "pattern_mismatch"
BUSINESS_RULE = "business_rule"
class ErrorCatalog:
def __init__(self):
self.messages = {
ErrorCode.REQUIRED: "{field} is required",
ErrorCode.INVALID_FORMAT: "{field} format is invalid",
ErrorCode.TYPE_ERROR: "{field} must be {expected}",
ErrorCode.MIN_LENGTH: "{field} must be at least {min} characters",
ErrorCode.MAX_LENGTH: "{field} must be at most {max} characters",
ErrorCode.MIN_VALUE: "{field} must be at least {min}",
ErrorCode.MAX_VALUE: "{field} must be at most {max}",
ErrorCode.NOT_UNIQUE: "{field} already exists",
ErrorCode.PATTERN_MISMATCH: "{field} format is invalid",
ErrorCode.BUSINESS_RULE: "{message}",
}
def format(self, code: str, **params) -> str:
template = self.messages.get(code, str(params.get("message", "")))
return template.format(**params)
def error(self, field: str, code: str, **params) -> Dict:
return {
"field": field,
"code": code,
"message": self.format(code, field=field, **params),
}
catalog = ErrorCatalog()
errors = [
catalog.error("email", ErrorCode.REQUIRED),
catalog.error("password", ErrorCode.MIN_LENGTH, min=8),
catalog.error("age", ErrorCode.MIN_VALUE, min=13),
catalog.error("username", ErrorCode.NOT_UNIQUE),
]
for e in errors:
print(f" [{e['code']:15s}] {e['message']}")
Expected output:
[required ] email is required
[min_length ] password must be at least 8 characters
[min_value ] age must be at least 13
[not_unique ] username already exists
Common Mistakes
1. Returning Only One Error at a Time
Clients must fix, retry, fail again, repeat. Return all errors so clients fix everything in one pass.
2. Leaking Internal Details
Error messages like "Database constraint violation on column users.email" expose internals.
3. Inconsistent Error Format
Some endpoints return {"error": "msg"}, others {"errors": ["msg"]}. Standardize.
4. No Error Codes
Messages change over time. Clients should parse error codes, not error strings.
5. Wrong HTTP Status Code
Validation errors use 400 Bad Request. Authentication errors use 401. Authorization uses 403.
Practice Questions
1. What HTTP status code for validation errors?
400 Bad Request.
2. Should error responses include error codes?
Yes. Clients parse codes, not messages. Codes are stable across message changes.
3. How do you handle nested field errors?
Use dot notation: address.street, address.zip. Nest error objects or use flat paths.
4. What is the problem with returning one error at a time?
Clients must make multiple requests to discover all errors. Return all errors at once.
Challenge
Build a validation error handler that converts a flat list of errors into nested field errors and supports localization of error messages.
FAQ
Mini Project: Error Builder
# error_builder.py
from typing import Any, Dict, List
class ErrorBuilder:
def __init__(self):
self.items: List[Dict] = []
def required(self, field: str):
self.items.append({"field": field, "code": "required"})
return self
def invalid(self, field: str, reason: str = "invalid"):
self.items.append({"field": field, "code": reason})
return self
def build(self) -> Dict:
return {
"status": 400,
"error": "Validation failed",
"details": self.items,
}
err = ErrorBuilder().required("email").invalid("password", "min_length").build()
print(err)
Expected output:
{'status': 400, 'error': 'Validation failed', 'details': [{'field': 'email', 'code': 'required'}, {'field': 'password', 'code': 'min_length'}]}
What's Next
You understand error handling. Next, learn async validation, then Express validation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro