Maximizing Reliability in FastAPI: Global exception handling

Maximizing Reliability in FastAPI: Global exception handling

With a focus on clean code.

Exception handling is an essential part of building reliable and robust web applications. In FastAPI, one way to catch and handle errors is by using global exception handling. In this tutorial, we'll focus specifically on how to catch and handle custom exceptions in FastAPI using global exception handling. By the end of this tutorial, you'll have a deep understanding of how to catch and handle custom exceptions effectively in FastAPI, and you'll be able to confidently build reliable applications without tons of boilerplate.

Why global exception handling?

Catching exceptions globally in a web application can be a useful way to reduce boilerplate and improve the reliability of your application. By globally catching exceptions, you can centralize your error-handling logic in a single place, rather than having to repeat it throughout your codebase. This can make your code easier to read and maintain, and can help to prevent errors from slipping through the cracks.

In addition, global exception handling can help to ensure that your application continues to function properly even in the face of errors. By catching and consistently handling exceptions, you can prevent errors from propagating to the client and can provide a more polished user experience. Overall, global exception handling is a powerful tool for building robust and reliable web applications.


A basic approach to handling exceptions

A basic approach to handling exceptions in a FastAPI application is to use try-except blocks in individual routes. For example, consider the following route in a FastAPI application that retrieves a Conference object from a database using a given ID:

@app.get("/conferences/{conference_id}")
async def get_conference(conference_id: str):
    try:
        return await get_conference_from_db(conference_id)
    except ObjectDoesNotExist as e:
        raise HTTPException(status_code=404, detail="Object not found")

In this route, the get_conference_from_db function is responsible for querying the database for a Conference object with the specified ID. If no such object is found, the ObjectDoesNotExist exception is raised. The route catches this exception and returns a 404 HTTP response code to the client, indicating that the requested resource was not found.

While this approach is valid, it can become cumbersome if you have multiple routes that need to handle the same exceptions. In this case, you would need to repeat the same try-except block in each route, which can violate the principles of clean code and make your application more difficult to maintain. To avoid this issue, you can use the global exception-handling mechanism provided by FastAPI [1] to centralize your error handling logic and reduce boilerplate.

Global exception handling

To handle exceptions globally in FastAPI, you can use the @app.exception_handler decorator to define a function that will be called whenever a specific exception is raised. This function should be defined in the main application module, which is typically called main.py and is the entry point for the application. The function should accept two arguments: the request object and the exception object. For example:

@app.exception_handler(ObjectDoestNotExist)
async def obj_not_exists_exception_handler(request: Request, exc: ObjectDoesNotExist):
    return JSONResponse(
        status_code=404,
        content={"message": "Object not found."},
    )

In this example, the obj_not_exists_exception_handler function is called whenever the ObjectDoestNotExist exception is raised in the application. The function returns a JSON response with a status code of 404 and a message indicating that the requested object was not found.

With this global exception handler in place, your routes can be simplified to focus on handling the request and triggering the appropriate functions, rather than worrying about error handling. For example:

@app.get("/conferences/{conference_id}")
async def get_conference(conference_id: str):
    return await get_conference_from_db(conference_id)

Additionally, all logic related to how to handle exceptions is in one place.

Clean code solution

As your project grows, you may find that main.py becomes cluttered with more global exception handlers. Additionally, it can become difficult to navigate main.py when it becomes larger. To address this issue, it may be a good idea to move the exception-handling function into a separate file. Unfortunately, FastAPI does not provide a convenient way to add custom exception handlers to the app by default. The only way to do this is to use the decorator and define the body of the function below it.

However, there is a better solution. FastAPI is built on top of Starlette, a lightweight ASGI framework that includes features such as request parsing, routing, and middleware support. Starlette defines the function add_exception_handler, which we can use because the FastAPI class derives from Starlette.

Starlette defines function add_exception_handler and because FastAPI class derives from Starlette we can use it the following way.

In main.py:

from fastapi import FastAPI
from app.exception_handlers import obj_not_exists_exception_handler

app = FastAPI()

app.add_exception_handler(ObjectDoestNotExist, obj_not_exists_exception_handler)

in exception_handlers.py:

async def obj_not_exists_exception_handler(request: Request, exc: ObjectDoesNotExist):
    return JSONResponse(
        status_code=404,
        content={"message": "Object not found."},
    )

routes.py:

async def get_conference_from_db(conference_id: str) -> Conference:
   ...

@app.get("/conferences/{conference_id}")
async def get_conference(conference_id: str):
    return await get_conference_from_db(conference_id)

As you can see, with that solution:

  • every function has a single responsibility,

  • every function belongs to the self-explaining files like exception_handlers.py ,

  • main.py is clean,

  • exceptions are handled globally and all logic is defined in only one file,

  • any changes to the error handling mechanism require changes only in *_exception_handler function.


References:

[1] https://fastapi.tiangolo.com/tutorial/handling-errors/

[2] https://www.starlette.io/

[3] https://github.com/encode/starlette/blob/0.23.1/starlette/applications.py#L146-L152