Skip to Content
OverviewException Filters

Exception Filters

Austial ships a built-in exceptions layer that converts thrown exceptions into a consistent JSON error response — no try/except in every handler required.

The default error response

Any unhandled exception becomes:

{ "statusCode": 404, "message": "Widget #999 not found", "error": "Not Found", "timestamp": "2026-07-12T10:15:00.000000+00:00", "path": "/widgets/999" }

statusCode/message/error/timestamp/path — the shape every default exception filter response uses.

Built-in HTTP exceptions

from austial import NotFoundException raise NotFoundException(f"Cat #{id} not found")
ClassStatus
BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
MethodNotAllowedException405
ConflictException409
UnprocessableEntityException422
TooManyRequestsException429
InternalServerErrorException500
NotImplementedException501
ServiceUnavailableException503

Every subclass takes the same constructor: (message: str | dict | None = None). Pass a string for a plain message, or a dict to control the body more precisely:

raise BadRequestException({"message": ["name is required"], "error": "Bad Request"})

Any of these raised from inside a handler (or from a guard/pipe/interceptor) is automatically caught and turned into the JSON shape above with the matching status code — you never construct the response yourself.

Writing a custom filter

from austial.common.filters import ArgumentsHost, Catch, ExceptionFilter from fastapi.responses import JSONResponse @Catch(NotFoundException) class NotFoundFilter(ExceptionFilter): async def catch(self, exception: NotFoundException, host: ArgumentsHost) -> JSONResponse: request = host.get_request() return JSONResponse( status_code=404, content={ "statusCode": 404, "message": str(exception), "path": request.url.path, "hint": "Check the ID and try again.", }, )

@Catch(*exceptions) declares which exception types a filter handles; @Catch() with no arguments catches everything.

Applying filters

app.use_global_filters(AllExceptionsFilter())

registers a filter application-wide — this is what every generated project’s src/main.py does with the built-in AllExceptionsFilter. @UseFilters(*filters) attaches filters to a single controller or handler instead, the same controller/handler-scoped pattern as guards, pipes, and interceptors:

from austial import UseFilters @Controller("cats") @UseFilters(NotFoundFilter()) class CatsController: ...

Dispatch order

When multiple filters are registered, Austial walks them in order and uses the first one whose @Catch(...) types match the raised exception (an empty @Catch() always matches). If none match, the exception re-raises — this is why AllExceptionsFilter (registered with an empty @Catch()) is the sane default to always have installed as a catch-all.

Validation errors raised by FastAPI itself (RequestValidationError) and any other uncaught exception are automatically funneled through the same filter chain, so a forgotten try/except still produces the same consistent error body instead of a raw traceback.

Last updated on