Error Handling
The errors module lets an application describe a failure once and return it in the format the client expects. An AppException carries the status code and structured error details, while the registered exception handler renders an HTML error page for a browser or a JSON:API-compliant response for an API client.
AppException
Raise AppException for application errors with structured details:
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
| Parameter | Type | Default | Description |
|---|---|---|---|
detail |
str |
required | Human-readable description of the error. |
status_code |
int |
400 |
HTTP status code. |
code |
ErrorCode | str |
None |
Machine-readable error code. |
source |
ErrorSource | None |
Where the error originated. |
meta |
dict |
None |
Additional metadata. |
ErrorCode
ErrorCode gives you machine-readable codes that follow the JSON:API convention:
from starlette_templates.errors import ErrorCode
ErrorCode.INTERNAL_ERROR # "internal_error"
ErrorCode.INVALID_REQUEST # "invalid_request"
ErrorCode.INVALID_PARAMETER # "invalid_parameter"
ErrorCode.NOT_FOUND # "not_found"
ErrorCode.VALIDATION_ERROR # "validation_error"
ErrorCode.UNAUTHORIZED # "unauthorized"
ErrorCode.FORBIDDEN # "forbidden"
ErrorCode.METHOD_NOT_ALLOWED # "method_not_allowed"
ErrorSource
ErrorSource identifies where an error occurred: a JSON pointer, a query or path parameter, or a request header.
from starlette_templates.errors import ErrorSource
ErrorSource(pointer="/data/attributes/email") # a location in the request body
ErrorSource(parameter="user_id") # a query or path parameter
ErrorSource(header="Authorization") # a request header
Wiring the handlers
Each handler is an async function. It takes the request, the exception, and a Jinja2 Environment that renders the error templates. Register the handlers with Starlette, binding your environment to each one:
from functools import partial
from jinja2 import Environment, FileSystemLoader
from starlette.applications import Starlette
from starlette.exceptions import HTTPException
from starlette_templates.errors import (
AppException,
exception_handler,
httpexception_handler,
)
env = Environment(
loader=FileSystemLoader("site"),
autoescape=True,
enable_async=True,
)
app = Starlette(
debug=False,
exception_handlers={
HTTPException: partial(httpexception_handler, jinja_env=env),
AppException: partial(exception_handler, jinja_env=env),
Exception: partial(exception_handler, jinja_env=env),
},
)
- httpexception_handler handles a Starlette
HTTPException, such as a 404 or a 405. - exception_handler handles an
AppException, a PydanticValidationError, and any other uncaught exception, which it renders as a 500.
Pass debug=True to a handler to add the traceback and request context to the rendered page.
Content negotiation
Each handler reads the request's Accept header:
- When the client accepts
application/json, the handler returns a JSON:API error response. - Otherwise the handler renders an HTML error page.
JSON:API response
raise AppException(
detail="Email address is already registered",
status_code=409,
code="conflict",
source=ErrorSource(pointer="/data/attributes/email"),
)
Returns:
{
"errors": [
{
"status": "409",
"code": "conflict",
"title": "AppException",
"detail": "Email address is already registered",
"source": { "pointer": "/data/attributes/email" }
}
]
}
The handler expands a Pydantic ValidationError into one JSON:API error per field. Each error carries a JSON pointer to the invalid field.
Error pages
For an HTML request, the handler looks for templates in this order. First it looks for a template named after the status code, such as 404.html or 500.html. Next it falls back to a generic error.html. Last, when neither template exists, it renders a minimal built-in page.
site/404.html:
<!DOCTYPE html>
<html>
<head><title>{{ status_code }} — {{ error_title }}</title></head>
<body>
<h1>{{ status_code }}</h1>
<h2>{{ error_title }}</h2>
<p>{{ error_message }}</p>
{% if structured_errors %}
<ul>
{% for error in structured_errors %}
<li><strong>{{ error.title or error.code }}</strong>: {{ error.detail }}</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
Template variables
| Variable | Description |
|---|---|
request |
The Starlette Request. |
status_code |
HTTP status code. |
error_title |
Short error summary. |
error_message |
Detailed error message. |
structured_errors |
A list of JSON:API error objects, such as per-field validation errors, when available. |
In debug mode, the context also carries the traceback and request details, so you can build a detailed debug page. These include frames, traceback_text, request_headers, query_params, and cookies.