Skip to content

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 Pydantic ValidationError, 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.