How Air's Default Exception Handling Works: A Deep Dive into HTML Error Pages

Air automatically registers styled HTML error pages for 404 and 500 HTTP errors by merging default handlers with user-provided callbacks during application initialization.

Air extends FastAPI's robust exception-handling system to deliver production-ready HTML error pages out of the box. When you create an Air application, it automatically wires up beautiful default handlers for the most common HTTP errors while preserving full flexibility for customization. This article examines the implementation details found in the feldroy/air repository to show exactly how these defaults are registered, merged, and dispatched.

Where Default Handlers Are Defined

Air ships with two primary exception handlers located in src/air/exception_handlers.py. These functions generate fully styled HTML responses using the built-in mvpcss layout system.

The default_404_exception_handler constructs a "Not Found" page with a title, heading, and description, returning an AirResponse with status code 404. Similarly, the default_500_exception_handler creates an internal server error page using the same layout components but returns status code 500. Both handlers accept a starlette.requests.Request object and a generic Exception instance, making them compatible with any raised error that maps to these HTTP status codes.

The Default Exception Handler Registry

The handlers are collected into a centralized dictionary that maps HTTP status codes to their corresponding callables. In src/air/exception_handlers.py, the library defines:

DEFAULT_EXCEPTION_HANDLERS: Final[ExceptionHandlersType] = {
    404: default_404_exception_handler,
    500: default_500_exception_handler,
}

This dictionary serves as the foundation of Air's default exception handling strategy. When an exception bubbles up through the application, FastAPI consults this mapping to determine which callable should render the response based on the error's status code or exception class.

How Air Merges Defaults with Custom Handlers

During application initialization in src/air/applications.py, Air intelligently merges its default handlers with any user-supplied exception handlers. The constructor accepts an optional exception_handlers argument. If none is provided, Air initializes an empty dictionary and then applies the defaults using the union operator:

if exception_handlers is None:
    exception_handlers = {}
exception_handlers = DEFAULT_EXCEPTION_HANDLERS | exception_handlers

This merge operation, found at lines 72-75 of src/air/applications.py, ensures that user-defined handlers take precedence while the default 404 and 500 pages remain available as fallbacks. The resulting dictionary is then passed to the underlying FastAPI instance as its exception_handlers parameter, allowing FastAPI's dispatcher to route uncaught exceptions to the appropriate handler.

Router-Level 404 Handling

For sub-routers that require isolated error handling, Air provides the default_404_router_handler function. This factory accepts a router_name: str parameter and returns an ASGI application that ensures "Not Found" errors within a specific router still render the consistent default HTML page.

The function constructs a starlette.requests.Request object, builds an HTTPException with status code 404, and delegates to the project-wide default_404_exception_handler. This design guarantees that whether a 404 originates from the main application or a mounted sub-router, users see the same styled error interface defined in src/air/exception_handlers.py.

Customizing Exception Handling

Air preserves FastAPI's full flexibility for exception handling while making customization straightforward at both the application and router levels.

Application-Wide Customization

Override the default handlers by passing a custom dictionary to the Air constructor or by using the decorator-based approach:

import air
from starlette.requests import Request

app = air.Air()  # Default 404/500 pages are active

@app.exception_handler(404)
def my_404_handler(request: Request, exc: Exception) -> air.AirResponse:
    return air.AirResponse(
        air.H1("Custom 404: Page not found"),
        status_code=404
    )

Router-Specific Customization

For sub-routers, apply handlers directly to the router instance or use default_404_router_handler to maintain the default UI while adjusting logic:

router = air.AirRouter()
router.get("/router-page")(lambda: air.P("Router page"))

# Option 1: Use default handler explicitly

router.exception_handler(404)(air.default_404_exception_handler)

# Option 2: Provide completely custom handler

@router.exception_handler(404)
def router_404(request: Request, exc: Exception):
    return air.AirResponse(air.P("Router-specific 404"), status_code=404)

app.include_router(router, prefix="/router")

Raising Exceptions That Trigger Defaults

Any route raising an HTTPException with status 404 or 500 automatically invokes the corresponding default handler:

@app.get("/missing")
def missing_item() -> air.AirResponse:
    raise air.HTTPException(status_code=404, detail="Item not found")

Summary

  • Default handlers for 404 and 500 errors live in src/air/exception_handlers.py and generate styled HTML using the mvpcss layout system.
  • The DEFAULT_EXCEPTION_HANDLERS dictionary maps status codes to callables, providing the foundation for Air's error response system.
  • Air merges defaults with user handlers in src/air/applications.py using the | operator, ensuring custom handlers override defaults while maintaining fallbacks.
  • Router-level handling via default_404_router_handler ensures consistent error pages across sub-routers.
  • Customization follows standard FastAPI patterns through constructor arguments or decorator-based registration.

Frequently Asked Questions

How do I override Air's default 404 page?

Pass a custom handler to the Air constructor or use the @app.exception_handler(404) decorator. Because Air merges dictionaries with DEFAULT_EXCEPTION_HANDLERS | exception_handlers, your custom handler takes precedence while other defaults remain intact.

Can I use Air's default error pages for some routes but custom pages for others?

Yes. Apply custom handlers to specific routers using router.exception_handler(404), or keep the default UI by explicitly registering air.default_404_exception_handler on routers where you want the standard styling. The main application can use different handlers entirely.

What exception class should I raise to trigger Air's default handlers?

Raise air.HTTPException (re-exported from FastAPI) with status_code=404 or status_code=500. Air's exception handlers in src/air/exception_handlers.py accept generic Exception objects, but FastAPI's dispatcher specifically looks for HTTPException instances to determine which status-code-based handler to invoke.

Where does Air store the logic for handling router-specific 404 errors?

The default_404_router_handler function in src/air/exception_handlers.py creates an ASGI application that wraps router-specific 404s into standard HTTPException instances and delegates them to the project-wide default_404_exception_handler. This ensures sub-routers render the same styled HTML as the main application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →