Error Messages — User-Friendly Errors, Localization, and Standards
In this tutorial, you'll learn about Error Messages. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Error messages communicate validation failures to users and clients. Well-designed errors are clear, actionable, consistent, and localizable — poor errors frustrate users and slow development.
What You'll Learn
By the end of this lesson, you will write user-friendly error messages, implement localization, design consistent error formats, and follow RFC 7807 Problem Details standards.
Why It Matters
A confusing error message causes support tickets, developer frustration, and lost users. A clear error message tells the user exactly what to fix and how. Error format consistency also enables automated client-side handling.
Real-World Use
Doda Browser's API returns RFC 7807 Problem Details for all validation errors, enabling its mobile clients to parse and display field-level errors in the user's language automatically.
Error Message Flow
flowchart LR
Input[User Input] --> Validate[Validate]
Validate --> Pass{Valid?}
Pass -->|Yes| Process[Process Request]
Pass -->|No| ErrorGen[Generate Error]
ErrorGen --> Format{Format Type}
Format -->|Simple| Simple[Field + Message]
Format -->|RFC 7807| RFC[Problem Details]
Format -->|Localized| L10n[Translate Messages]
Simple --> Response[API Response]
RFC --> Response
L10n --> Response
User-Friendly Error Builder
# error_builder.py
from typing import Any, Dict, List, Optional
class ErrorBuilder:
@staticmethod
def friendly_message(field: str, code: str) -> str:
messages = {
"required": f"{field} is required. Please fill in this field.",
"type_error": f"{field} has the wrong type. Check the expected format.",
"min_length": f"{field} is too short. Make it longer.",
"max_length": f"{field} is too long. Shorten the value.",
"min_value": f"{field} is too low. Increase the value.",
"max_value": f"{field} is too high. Decrease the value.",
"email": f"{field} does not look like a valid email address.",
"pattern": f"{field} has an invalid format. Check the requirements.",
"unique": f"{field} is already taken. Choose a different value.",
"password_mismatch": "Passwords do not match. Retype both carefully.",
}
return messages.get(code, f"{field} is invalid.")
@staticmethod
def build_error(field: str, code: str, detail: Optional[str] = None) -> Dict:
return {
"field": field,
"code": code,
"message": detail or ErrorBuilder.friendly_message(field, code),
}
@staticmethod
def build_errors(errors: List[Dict]) -> Dict:
return {
"valid": len(errors) == 0,
"count": len(errors),
"errors": errors,
}
vb = ErrorBuilder()
errors = [
vb.build_error("email", "required"),
vb.build_error("password", "min_length", "Password must be at least 8 characters"),
vb.build_error("confirm_password", "password_mismatch"),
]
result = vb.build_errors(errors)
print(f"Valid: {result['valid']}")
print(f"Count: {result['count']}")
for err in result["errors"]:
print(f" [{err['code']}] {err['field']}: {err['message']}")
Expected output:
Valid: False
Count: 3
[required] email: email is required. Please fill in this field.
[min_length] password: Password must be at least 8 characters
[password_mismatch] confirm_password: Passwords do not match. Retype both carefully.
RFC 7807 Problem Details
# rfc7807_errors.py
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
@dataclass
class ValidationProblem:
type: str = "about:blank"
title: str = "Validation Error"
status: int = 422
detail: str = "One or more fields failed validation"
instance: Optional[str] = None
errors: List[Dict] = field(default_factory=list)
def to_dict(self) -> Dict:
result = {
"type": self.type,
"title": self.title,
"status": self.status,
"detail": self.detail,
"errors": self.errors,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
if self.instance:
result["instance"] = self.instance
return result
def build_rfc7807_response(errors: List[Dict], instance: Optional[str] = None) -> Dict:
problem = ValidationProblem(
type="https://api.example.com/errors/validation",
title="Validation Failed",
status=422,
detail=f"The request contains {len(errors)} validation error(s)",
instance=instance,
errors=errors,
)
return problem.to_dict()
rfc_errors = [
{"field": "email", "code": "format", "message": "Must be a valid email address"},
{"field": "age", "code": "range", "message": "Age must be between 13 and 150"},
]
response = build_rfc7807_response(rfc_errors, "/api/users")
print(f"Status: {response['status']}")
print(f"Title: {response['title']}")
print(f"Detail: {response['detail']}")
print(f"Instance: {response['instance']}")
for err in response["errors"]:
print(f" {err['field']}: {err['message']}")
Expected output:
Status: 422
Title: Validation Failed
Detail: The request contains 2 validation error(s)
Instance: /api/users
email: Must be a valid email address
age: Age must be between 13 and 150
Localization (i18n) System
# localization.py
from typing import Dict, List, Optional
class LocalizationEngine:
def __init__(self):
self.messages: Dict[str, Dict[str, str]] = {}
self.fallback_locale = "en"
def load(self, locale: str, messages: Dict[str, str]):
self.messages[locale] = messages
def translate(self, code: str, locale: str = "en", **kwargs) -> str:
locale_messages = self.messages.get(locale, self.messages.get(self.fallback_locale, {}))
template = locale_messages.get(code, locale_messages.get("fallback", "Validation error"))
try:
return template.format(**kwargs)
except KeyError:
return template
def localize_error(self, error: Dict, locale: str = "en") -> Dict:
localized = dict(error)
code = error.get("code", "unknown")
field = error.get("field", "field")
localized["message"] = self.translate(code, locale, field=field)
return localized
engine = LocalizationEngine()
engine.load("en", {
"required": "{field} is required",
"min_length": "{field} must be at least {min} characters",
"email": "{field} must be a valid email",
"fallback": "Invalid value for {field}",
})
engine.load("es", {
"required": "{field} es obligatorio",
"min_length": "{field} debe tener al menos {min} caracteres",
"email": "{field} debe ser un correo valido",
"fallback": "Valor invalido para {field}",
})
engine.load("fr", {
"required": "{field} est requis",
"min_length": "{field} doit contenir au moins {min} caracteres",
"email": "{field} doit etre un email valide",
"fallback": "Valeur invalide pour {field}",
})
errors = [
{"field": "email", "code": "required"},
{"field": "password", "code": "min_length", "min": 8},
]
for locale in ["en", "es", "fr"]:
print(f"\nLocale: {locale}")
for err in errors:
localized = engine.localize_error(err, locale)
print(f" {localized['field']}: {localized['message']}")
Expected output:
Locale: en
email: email is required
password: password must be at least 8 characters
Locale: es
email: email es obligatorio
password: password debe tener al menos 8 caracteres
Locale: fr
email: email est requis
password: password doit contenir au moins 8 caracteres
Field Mapping for Nested Errors
# field_mapping.py
from typing import Any, Dict, List, Optional
class FieldMapper:
def __init__(self):
self.mappings: Dict[str, str] = {}
def map(self, internal: str, external: str):
self.mappings[internal] = external
def to_external(self, internal_field: str) -> str:
return self.mappings.get(internal_field, internal_field)
def remap_errors(self, errors: List[Dict]) -> List[Dict]:
remapped = []
for err in errors:
new_err = dict(err)
new_err["field"] = self.to_external(err.get("field", ""))
remapped.append(new_err)
return remapped
@staticmethod
def flatten_nested(data: Dict, prefix: str = "") -> List[Dict]:
errors = []
for key, value in data.items():
field_name = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
if "message" in value or "code" in value:
errors.append({"field": field_name, **value})
else:
errors.extend(FieldMapper.flatten_nested(value, field_name))
elif isinstance(value, list):
for i, item in enumerate(value):
nested_field = f"{field_name}[{i}]"
if isinstance(item, dict):
errors.extend(FieldMapper.flatten_nested(item, nested_field))
else:
errors.append({"field": nested_field, "message": str(item)})
else:
errors.append({"field": field_name, "message": str(value)})
return errors
mapper = FieldMapper()
mapper.map("user.email", "email_address")
mapper.map("user.pass", "password")
raw_errors = [
{"field": "user.email", "code": "required"},
{"field": "user.pass", "code": "min_length", "message": "Password too short"},
]
remapped = mapper.remap_errors(raw_errors)
for err in remapped:
print(f" {err['field']}: {err.get('message', err['code'])}")
nested = {
"user": {
"name": {"code": "required"},
"address": {
"street": {"code": "required"},
"city": {"code": "required"},
}
}
}
flat = FieldMapper.flatten_nested(nested)
print("\nFlattened nested errors:")
for err in flat:
print(f" {err['field']}: {err['code']}")
Expected output:
email_address: required
password: Password too short
Flattened nested errors:
user.name: required
user.address.street: required
user.address.city: required
Error Response Standards
# error_standards.py
from typing import Any, Dict, List, Optional
class ErrorResponseFormatter:
@staticmethod
def simple(errors: List[Dict]) -> Dict:
return {
"success": False,
"error": {
"message": "Validation failed",
"details": errors,
}
}
@staticmethod
def flat(errors: List[Dict]) -> Dict:
result = {"success": False, "errors": {}}
for err in errors:
field = err.get("field", "general")
result["errors"][field] = err.get("message", str(err))
return result
@staticmethod
def array(errors: List[Dict]) -> Dict:
return {
"success": False,
"errors": [{"field": e.get("field"), "message": e.get("message")} for e in errors]
}
@staticmethod
def graphql_format(errors: List[Dict]) -> List[Dict]:
return [
{
"message": e.get("message", "Validation error"),
"extensions": {
"code": e.get("code", "VALIDATION_ERROR"),
"field": e.get("field"),
}
}
for e in errors
]
fmt = ErrorResponseFormatter()
sample_errors = [
{"field": "email", "code": "required", "message": "Email is required"},
{"field": "password", "code": "min_length", "message": "Minimum 8 characters"},
]
print("Simple format:")
print(f" {fmt.simple(sample_errors)}")
print("Flat format:")
print(f" {fmt.flat(sample_errors)}")
print("Array format:")
print(f" {fmt.array(sample_errors)}")
print("GraphQL format:")
for err in fmt.graphql_format(sample_errors):
print(f" {err}")
Expected output:
Simple format:
{'success': False, 'error': {'message': 'Validation failed', 'details': [{'field': 'email', 'code': 'required', 'message': 'Email is required'}, {'field': 'password', 'code': 'min_length', 'message': 'Minimum 8 characters'}]}}
Flat format:
{'success': False, 'errors': {'email': 'Email is required', 'password': 'Minimum 8 characters'}}
Array format:
{'success': False, 'errors': [{'field': 'email', 'message': 'Email is required'}, {'field': 'password', 'message': 'Minimum 8 characters'}]}
GraphQL format:
{'message': 'Email is required', 'extensions': {'code': 'required', 'field': 'email'}}
{'message': 'Minimum 8 characters', 'extensions': {'code': 'min_length', 'field': 'password'}}
Common Mistakes
1. Technical Jargon in Messages
Messages like "Field 'email' failed regex pattern validation" are useless to end users. Write "Enter a valid email address."
2. Inconsistent Error Format
One endpoint returns {errors: [{field, message}]}, another returns {error: "msg"}. Standardize across your API.
3. Exposing Internal Field Names
Sending user.profile.account.email as the field path leaks schema details. Map to external names like email_address.
4. No Error Codes
Clients need stable error codes (like min_length) to handle errors programmatically. Parsing message strings is fragile.
5. Not Localizing at All
Returning English-only errors excludes non-English-speaking users. Support at least a locale query parameter.
Practice Questions
1. What is the RFC 7807 standard?
Problem Details for HTTP APIs — a standardized format for error responses with type, title, status, detail, and instance fields.
2. Why use error codes instead of messages for client logic?
Error codes are stable identifiers. Message strings change with localization and are fragile for programmatic handling.
3. What is field mapping?
Translating internal field names (user.profile.email) to external names (email_address) in error responses to hide schema details.
4. How do you localize validation errors?
Use a message catalog per locale with template strings. Accept a locale parameter and translate error codes at response time.
Challenge
Build a complete error response system supporting RFC 7807 with localization (en/es/fr), field mapping, nested error flattening, and multiple output formats (REST, Graphql, JSON:API).
FAQ
Mini Project: Localized Error Response Handler
# error_handler.py
from typing import Any, Dict, List, Optional
class ErrorHandler:
def __init__(self):
self.locales = {
"en": {"required": "{field} is required", "email": "Invalid email"},
"es": {"required": "{field} es obligatorio", "email": "Email invalido"},
}
self.field_map = {"user_email": "email", "user_pass": "password"}
def translate(self, code: str, field: str, locale: str = "en") -> str:
msgs = self.locales.get(locale, self.locales["en"])
template = msgs.get(code, f"Invalid {field}")
return template.format(field=field)
def format_error(self, field: str, code: str, locale: str = "en") -> Dict:
external = self.field_map.get(field, field)
return {
"field": external,
"code": code,
"message": self.translate(code, external, locale),
}
def handle(self, raw_errors: List[Dict], locale: str = "en") -> Dict:
formatted = [self.format_error(e["field"], e["code"], locale) for e in raw_errors]
return {"valid": False, "count": len(formatted), "errors": formatted}
handler = ErrorHandler()
raw = [{"field": "user_email", "code": "required"}, {"field": "user_pass", "code": "required"}]
print(handler.handle(raw, "en"))
print(handler.handle(raw, "es"))
Expected output:
{'valid': False, 'count': 2, 'errors': [{'field': 'email', 'code': 'required', 'message': 'email is required'}, {'field': 'password', 'code': 'required', 'message': 'password is required'}]}
{'valid': False, 'count': 2, 'errors': [{'field': 'email', 'code': 'required', 'message': 'email es obligatorio'}, {'field': 'password', 'code': 'required', 'message': 'password es obligatorio'}]}
What's Next
You understand error messages. Next, learn validation security, then honeypot validation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro