Error Handling
This guide covers error handling, custom exceptions, error pages, and JSON:API error responses.
Overview
Starlette Templates provides comprehensive error handling with:
- Custom exception classes with structured error details
- Automatic HTML error pages for browser requests
- JSON:API compliant error responses for API requests
- Custom error page templates
- Context processors for error pages
Custom Exceptions
AppException
AppException is the base exception class for structured error responses.
from starlette_templates.errors import AppException, ErrorCode, ErrorSource
async def get_user(user_id: int):
if not user_exists(user_id):
raise AppException(
detail=f"User with ID {user_id} not found",
status_code=404,
code=ErrorCode.NOT_FOUND,
source=ErrorSource(parameter="user_id"),
meta={"user_id": user_id}
)
Parameters
- detail (str, required) - Human-readable explanation of the error
- status_code (int, default: 500) - HTTP status code
- code (ErrorCode, optional) - Machine-readable error code
- source (ErrorSource, optional) - Source of the error
- title (str, optional) - Short, human-readable error summary
- meta (dict, optional) - Additional metadata about the error
ErrorCode
ErrorCode provides standard error codes:
from starlette_templates.errors import ErrorCode
ErrorCode.BAD_REQUEST # 400
ErrorCode.UNAUTHORIZED # 401
ErrorCode.FORBIDDEN # 403
ErrorCode.NOT_FOUND # 404
ErrorCode.METHOD_NOT_ALLOWED # 405
ErrorCode.CONFLICT # 409
ErrorCode.VALIDATION_ERROR # 422
ErrorCode.INTERNAL_SERVER_ERROR # 500
ErrorSource
ErrorSource identifies where the error occurred:
from starlette_templates.errors import ErrorSource
# Error in a request parameter
ErrorSource(parameter="user_id")
# Error in a request header
ErrorSource(header="Authorization")
# Error in a JSON pointer location
ErrorSource(pointer="/data/attributes/email")
# Error in a query parameter
ErrorSource(parameter="page[limit]")
Error Pages
Create custom error pages by adding templates to your template directory.
404.html - Not Found
<!DOCTYPE html>
<html>
<head>
<title>{{ status_code }} - {{ error_title }}</title>
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
</head>
<body>
<div class="error-page">
<h1>{{ status_code }}</h1>
<h2>{{ error_title }}</h2>
<p>{{ error_message }}</p>
<a href="{{ url_for('home') }}" class="btn">Go Home</a>
</div>
</body>
</html>
500.html - Server Error
<!DOCTYPE html>
<html>
<head>
<title>{{ status_code }} - {{ error_title }}</title>
</head>
<body>
<div class="error-page">
<h1>{{ status_code }}</h1>
<h2>{{ error_title }}</h2>
<p>{{ error_message }}</p>
{% if structured_errors %}
<div class="error-details">
<h3>Error Details:</h3>
<ul>
{% for error in structured_errors %}
<li>
<strong>{{ error.title or error.code }}</strong>: {{ error.detail }}
{% if error.source %}
<br><small>Source: {{ error.source }}</small>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if request.app.debug %}
<div class="debug-info">
<h3>Debug Information:</h3>
<pre>{{ traceback }}</pre>
</div>
{% endif %}
</div>
</body>
</html>
error.html - Generic Error Page
Generic fallback for any error without a specific template:
<!DOCTYPE html>
<html>
<head>
<title>Error - {{ status_code }}</title>
</head>
<body>
<div class="error-page">
<h1>{{ status_code }}</h1>
<h2>{{ error_title }}</h2>
<p>{{ error_message }}</p>
<a href="/">Go Home</a>
</div>
</body>
</html>
Available Template Variables
All error templates have access to:
status_code(int) - HTTP status codeerror_title(str) - Short error summaryerror_message(str) - Detailed error explanationstructured_errors(list) - List of error objects with structured detailsrequest- The Starlette Request objecttraceback(str) - Stack trace (only in debug mode)
JSON:API Error Responses
For API requests (requests with Accept: application/json or Content-Type: application/json), errors are automatically returned as JSON:API compliant responses.
Single Error
from starlette_templates.errors import AppException, ErrorCode
async def register_user(email: str):
raise AppException(
detail="Email address is already registered",
status_code=409,
code=ErrorCode.CONFLICT,
source=ErrorSource(pointer="/data/attributes/email"),
)
Returns:
{
"errors": [
{
"status": "409",
"code": "conflict",
"title": "Conflict",
"detail": "Email address is already registered",
"source": {
"pointer": "/data/attributes/email"
}
}
]
}
Multiple Errors
from starlette_templates.errors import AppException, ErrorCode, ErrorSource
async def validate_user_input(data: dict):
# Validation error with multiple field errors
raise AppException(
detail="Validation failed",
status_code=422,
code=ErrorCode.VALIDATION_ERROR,
errors=[
{
"detail": "Email address is required",
"source": ErrorSource(pointer="/data/attributes/email"),
"code": "required"
},
{
"detail": "Password must be at least 8 characters",
"source": ErrorSource(pointer="/data/attributes/password"),
"code": "min_length"
}
]
)
Returns:
{
"errors": [
{
"status": "422",
"code": "required",
"detail": "Email address is required",
"source": {
"pointer": "/data/attributes/email"
}
},
{
"status": "422",
"code": "min_length",
"detail": "Password must be at least 8 characters",
"source": {
"pointer": "/data/attributes/password"
}
}
]
}
Error Response Format
JSON:API error responses follow the JSON:API specification:
{
"errors": [
{
"status": "404",
"code": "not_found",
"title": "Page Not Found",
"detail": "The requested resource was not found",
"source": {
"parameter": "user_id"
},
"meta": {
"user_id": 123,
"timestamp": "2025-12-20T12:00:00Z"
}
}
]
}
Custom Error Handlers
Application-level Error Handler
Provide a custom error handler for [TemplateFiles][starlette_templates.templating.TemplateFiles]:
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from starlette_templates.templating import TemplateFiles
import logging
logger = logging.getLogger(__name__)
async def custom_error_handler(request: Request, exc: Exception) -> Response:
# Log the error
logger.error(
f"Error processing {request.url}: {exc}",
exc_info=exc,
extra={
"url": str(request.url),
"method": request.method,
"client": request.client.host if request.client else None,
}
)
# Send notification for critical errors
if isinstance(exc, CriticalError):
await send_alert_notification(exc)
# Return custom response
if "application/json" in request.headers.get("accept", ""):
return JSONResponse(
{"error": "Something went wrong"},
status_code=500
)
return TemplateResponse(
"error.html",
context={
"status_code": 500,
"error_title": "Internal Server Error",
"error_message": "An unexpected error occurred",
},
status_code=500
)
templates = TemplateFiles(
error_handler=custom_error_handler
)
Route-level Error Handling
Handle errors within specific routes:
from starlette.routing import Route
from starlette.requests import Request
from starlette_templates.forms import FormModel
from starlette_templates.responses import TemplateResponse
from starlette_templates.errors import AppException, ErrorCode
class User(FormModel):
user_id: int
async def user_profile(request: Request) -> TemplateResponse:
user = await User.from_request(request)
try:
# Fetch user profile, may raise exceptions UserNotFound, PermissionDenied
user = await get_user(user.user_id)
return TemplateResponse("profile.html", context={"user": user})
except UserNotFound:
raise AppException(
detail=f"User {user.user_id} not found",
status_code=404,
code=ErrorCode.NOT_FOUND,
)
except PermissionDenied:
raise AppException(
detail="You don't have permission to view this profile",
status_code=403,
code=ErrorCode.FORBIDDEN,
)
app = Starlette(
routes=[
Route("/users/{user_id}", user_profile),
]
)
Form Validation Errors
Handle Pydantic validation errors from forms:
from pydantic import ValidationError
from starlette.requests import Request
from starlette_templates.responses import TemplateResponse
from starlette_templates.forms import FormModel
async def signup(request: Request) -> TemplateResponse:
try:
form = await SignupForm.from_request(request, raise_on_error=True)
if form.is_valid(request):
user = await create_user(form)
return TemplateResponse("success.html", context={"user": user})
except ValidationError as e:
# Return form with validation errors
return TemplateResponse(
"signup.html",
context={
"errors": e.errors(),
"error_message": "Please correct the errors below",
},
status_code=400
)
# Show empty form for GET requests
return TemplateResponse("signup.html", context={"form": SignupForm()})
Error Page Context Processors
Add custom context to error pages:
from starlette.requests import Request
async def add_error_context(request: Request) -> dict:
return {
"support_email": "[email protected]",
"status_page_url": "https://status.example.com",
"request_id": request.headers.get("X-Request-ID"),
}
templates = TemplateFiles(
context_processors=[add_error_context]
)
Use in error templates:
<div class="error-page">
<h1>{{ status_code }}</h1>
<p>{{ error_message }}</p>
<div class="error-support">
<p>Need help? Contact us at <a href="mailto:{{ support_email }}">{{ support_email }}</a></p>
<p>Request ID: <code>{{ request_id }}</code></p>
<p>Check service status: <a href="{{ status_page_url }}">{{ status_page_url }}</a></p>
</div>
</div>