Skip to content

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 code
  • error_title (str) - Short error summary
  • error_message (str) - Detailed error explanation
  • structured_errors (list) - List of error objects with structured details
  • request - The Starlette Request object
  • traceback (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": "support@example.com",
        "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>