Errors

Error hierarchy, error handlers, and debug pages

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Chirp's exceptions all inherit fromChirpError. You have two jobs: raise an HTTP error (NotFound, HTTPError) inside a handler to send an error response, and register@app.errorhandlers to control what the reader sees. This page is the reference for the error types, the handler signatures, and the dev-mode error output.

Error hierarchy

All Chirp exceptions subclassChirpError. The HTTP errors are frozen dataclasses that carry astatus code and a detailstring.

Exception Status Raise it when
ChirpError Base class. Catch it to handle any Chirp error.
HTTPError any You want to return a specific status:raise HTTPError(403, "Forbidden").
NotFound 404 A resource does not exist. Subclass ofHTTPError.
MethodNotAllowed 405 Raised automatically by the router when the path matches but the method does not.
PayloadTooLarge 413 Raised automatically when a request body or upload exceeds a configured size limit.
ConfigurationError The app is misconfigured. Raised at startup, before any request is served.

Raising errors

Raise HTTP errors in route handlers to trigger error responses:

from chirp import NotFound, HTTPError, g

@app.route("/users/{id:int}")
async def get_user(id: int):
    user = await db.fetch_one("SELECT * FROM users WHERE id = ?", [id])
    if not user:
        raise NotFound(f"User {id} not found")
    return Template("user.html", user=user)

@app.route("/premium")
def premium():
    # g.user / .is_premium here is your own app state, not a Chirp API.
    if not g.user or not g.user.is_premium:
        raise HTTPError(403, "Premium access required")
    return Template("premium.html")

gis Chirp's request-scoped state — populate it from your auth middleware.

NotFound

raise NotFound("Page not found")     # 404 with detail message
raise NotFound()                       # 404 with default message

MethodNotAllowed

Raised automatically by the router when a path matches but the HTTP method does not. The response includes anAllowheader listing valid methods and the allowed methods in the body.

ConfigurationError

Raised at startup, before any request is served, for invalid configuration:

# These raise ConfigurationError:
# - Adding SessionMiddleware without a secret_key
# - Adding CSRFMiddleware without SessionMiddleware
# - Returning Template/Fragment when kida integration is not configured

Most startup misconfiguration surfaces through app.check()rather than a raw exception. See startup contract checks for the full catalog of what is validated and how severity is decided.

Error handlers

Register custom error handlers by status code or exception type:

@app.error(404)
def handle_404(request: Request):
    return Template("errors/404.html", path=request.path)

@app.error(500)
def handle_500(request: Request, error: Exception):
    return Template("errors/500.html", error=str(error))

Error handlers support the same return-value system as route handlers. You can return a Template, Fragment, Response, string, or dict.

Handler signatures

Chirp inspects the handler signature and injects the appropriate arguments — pick whichever fits. Sync and async handlers both work.

@app.error(404)
def handle_404():
    return "Not Found"
@app.error(404)
def handle_404(request: Request):
    return Template("404.html", path=request.path)
@app.error(500)
def handle_500(request: Request, error: Exception):
    log_error(error)
    return Template("500.html")

Exception-type handlers

Register a handler keyed on an exception class instead of a status code. The key must be a real exception youraise. Chirp matches type(exc)first, then falls back to the status code, so a type handler lets you centralize the response for one domain error in one place:

from chirp import HTTPError

class PaymentRequired(HTTPError):
    def __init__(self, detail: str = "Payment required"):
        super().__init__(402, detail)

@app.error(PaymentRequired)
def handle_payment(request: Request, error: PaymentRequired):
    return Template("errors/payment.html", detail=error.detail)

Raise PaymentRequired(...)anywhere in a handler and this handler renders the response. Because the lookup is type-first, it wins over a@app.error(402) status handler for that specific class.

Fragment-aware error handling

When an htmx request triggers an error, you usually want to swap a small error fragment into the page instead of replacing it with a full error document. The simplest way is to return aPage and let Chirp negotiate: it renders the named block for a narrow htmx swap and the full page for a browser navigation.

@app.error(404)
def handle_404(request: Request):
    return Page("errors/404.html", "error_message", path=request.path)

If you need to branch explicitly, check request.is_narrow_fragmentTrue only for a narrow swap,Falsefor boosted navigations and history restores that still need full page content:

@app.error(404)
def handle_404(request: Request):
    if request.is_narrow_fragment:
        return Fragment("errors/404.html", "error_message", path=request.path)
    return Template("errors/404.html", path=request.path)

When you do not register a handler, built-in error handling automatically returns a<div class="chirp-error">snippet for htmx requests and a full document otherwise.

Dev-mode error output

During development, Chirp formats errors for the terminal with structured, readable output instead of raw Python tracebacks. You rarely configure this — it is on automatically indebugmode. The detail below is for when you want to change the verbosity or understand how streaming errors are surfaced.

Debug pages

When AppConfig(debug=True), unhandled exceptions render a full HTML debug page in the browser with the traceback (framework frames collapsed), request details, app configuration, a Kida template error panel, and an environment section showing the Python, Chirp, and Kida versions. Consecutive framework frames are folded into an expandable block to reduce noise.