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

> Explore how Air's default exception handling registers styled HTML error pages for 404 and 500 errors merging default handlers with your custom callbacks during initialization.

- Repository: [Feldroy/air](https://github.com/feldroy/air)
- Tags: deep-dive
- Published: 2026-03-01

---

**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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/src/air/exception_handlers.py), the library defines:

```python
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`](https://github.com/feldroy/air/blob/main/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:

```python
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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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:

```python
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:

```python
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:

```python
@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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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`](https://github.com/feldroy/air/blob/main/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.