Exceptions and exception handling#

Litestar define a base exception called LitestarException which serves as a basis to all other exceptions.

In general, Litestar will raise two types of exceptions:

  • Exceptions that arise during application init, which fall

  • Exceptions that are raised as part of the normal application flow, i.e. exceptions in route handlers, dependencies, and middleware, that should be serialized in some fashion.

Configuration Exceptions#

For missing extra dependencies, Litestar will raise either MissingDependencyException. For example, if you try to use the SQLAlchemyPlugin without having SQLAlchemy installed, this will be raised when you start the application.

For other configuration issues, Litestar will raise ImproperlyConfiguredException with a message explaining the issue.

Application Exceptions#

For application exceptions, Litestar uses the class HTTPException, which inherits from LitestarException. This exception will be serialized into a JSON response of the following schema:

{
  "status_code": 500,
  "detail": "Internal Server Error",
  "extra": {}
}

Litestar also offers several pre-configured exception subclasses with pre-set error codes that you can use, such as:

Exception

Status code

Description

ImproperlyConfiguredException

500

Used internally for configuration errors

ValidationException

400

Raised when validation or parsing failed

NotFoundException

404

HTTP status code 404

NotAuthorizedException

401

HTTP status code 401

PermissionDeniedException

403

HTTP status code 403

InternalServerException

500

HTTP status code 500

ServiceUnavailableException

503

HTTP status code 503

When a value fails pydantic validation, the result will be a ValidationException with the extra key set to the pydantic validation errors. Thus, this data will be made available for the API consumers by default.

Exception handling#

Litestar handles all errors by default by transforming them into JSON responses. If the errors are instances of HTTPException, the responses will include the appropriate status_code. Otherwise, the responses will default to 500 - "Internal Server Error".

You can customize exception handling by passing a dictionary, mapping either status codes or exception classes to callables. For example, if you would like to replace the default exception handler with a handler that returns plain-text responses you could do this:

from litestar import Litestar, MediaType, Request, Response, get
from litestar.exceptions import HTTPException
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR


def plain_text_exception_handler(_: Request, exc: Exception) -> Response:
    """Default handler for exceptions subclassed from HTTPException."""
    status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
    detail = getattr(exc, "detail", "")

    return Response(
        media_type=MediaType.TEXT,
        content=detail,
        status_code=status_code,
    )


@get("/")
async def index() -> None:
    raise HTTPException(detail="an error occurred", status_code=400)


app = Litestar(
    route_handlers=[index],
    exception_handlers={HTTPException: plain_text_exception_handler},
)

Run it

> curl http://127.0.0.1:8000/
an error occurred

The above will define a top level exception handler that will apply the plain_text_exception_handler function to all exceptions that inherit from HTTPException. You could of course be more granular:

from litestar import Litestar, MediaType, Request, Response, get
from litestar.exceptions import HTTPException, ValidationException
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR


def validation_exception_handler(request: Request, exc: ValidationException) -> Response:
    return Response(
        media_type=MediaType.TEXT,
        content=f"validation error: {exc.detail}",
        status_code=400,
    )


def internal_server_error_handler(request: Request, exc: Exception) -> Response:
    return Response(
        media_type=MediaType.TEXT,
        content=f"server error: {exc}",
        status_code=500,
    )


def value_error_handler(request: Request, exc: ValueError) -> Response:
    return Response(
        media_type=MediaType.TEXT,
        content=f"value error: {exc}",
        status_code=400,
    )


@get("/validation-error")
async def validation_error(some_query_param: str) -> str:
    return some_query_param


@get("/server-error")
async def server_error() -> None:
    raise HTTPException()


@get("/value-error")
async def value_error() -> None:
    raise ValueError("this is wrong")


app = Litestar(
    route_handlers=[validation_error, server_error, value_error],
    exception_handlers={
        ValidationException: validation_exception_handler,
        HTTP_500_INTERNAL_SERVER_ERROR: internal_server_error_handler,
        ValueError: value_error_handler,
    },
)

Run it

> curl http://127.0.0.1:8000/validation-error
validation error: Missing required query parameter 'some_query_param' for url http://127.0.0.1:58147/validation-error
> curl http://127.0.0.1:8000/server-error
server error: 500: Internal Server Error
> curl http://127.0.0.1:8000/value-error
value error: this is wrong

The choice whether to use a single function that has switching logic inside it, or multiple functions depends on your specific needs.

While it does not make much sense to have different functions with a top-level exception handling, Litestar supports defining exception handlers on all layers of the app, with the lower layers overriding layer above them. In the following example, the exception handler for the route handler function will only handle the ValidationException occurring within that route handler:

from litestar import Litestar, Request, Response, get
from litestar.exceptions import HTTPException, ValidationException


def app_exception_handler(request: Request, exc: HTTPException) -> Response:
    return Response(
        content={
            "error": "server error",
            "path": request.url.path,
            "detail": exc.detail,
            "status_code": exc.status_code,
        },
        status_code=500,
    )


def router_handler_exception_handler(request: Request, exc: ValidationException) -> Response:
    return Response(
        content={"error": "validation error", "path": request.url.path},
        status_code=400,
    )


@get("/")
async def index() -> None:
    raise HTTPException("something's gone wrong")


@get(
    "/greet",
    exception_handlers={ValidationException: router_handler_exception_handler},
)
async def greet(name: str) -> str:
    return f"hello {name}"


app = Litestar(
    route_handlers=[index, greet],
    exception_handlers={HTTPException: app_exception_handler},
)

Run it

> curl http://127.0.0.1:8000/
{"error":"server error","path":"/","detail":"something's gone wrong","status_code":500}
> curl http://127.0.0.1:8000/greet
{"error":"validation error","path":"/greet"}

Exception handling layers#

Since Litestar allows users to define both exception handlers and middlewares in a layered fashion, i.e. on individual route handlers, controllers, routers, or the app layer, multiple layers of exception handlers are required to ensure that exceptions are handled correctly:

../_images/exception-handlers.jpg

Exception Handlers#

As a result of the above structure, the exceptions raised by the ASGI Router itself, namely 404 Not Found and 405 Method Not Allowed are handled only by exception handlers defined on the app layer. Thus, if you want to affect these exceptions, you will need to pass the exception handlers for them to the Litestar constructor and cannot use other layers for this purpose.