Skip to main content

FastAPI Exception Handlers

When building web applications with FastAPI, handling errors gracefully is crucial for providing a good user experience and maintaining reliable systems. Exception handlers allow you to intercept errors that occur during request processing and customize how your API responds to these errors.

Introduction to Exception Handling in FastAPI

In any web application, things can go wrong. A user might input invalid data, a database connection might fail, or your code might encounter an unexpected situation. Instead of letting your application crash or return generic error messages, FastAPI provides a robust exception handling system that allows you to:

  • Return customized error responses
  • Format error messages consistently across your application
  • Log and track exceptions for debugging
  • Maintain a professional API interface even when errors occur

Let's explore how FastAPI's exception handling works and how you can leverage it in your applications.

Understanding HTTP Exceptions

FastAPI comes with built-in HTTP exceptions through the HTTPException class, which is the foundation of error handling in FastAPI applications.

Basic HTTP Exception Usage

python
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
items = {1: "Hammer", 2: "Screwdriver", 3: "Wrench"}

if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")

return {"item": items[item_id]}

In this example, if a user requests an item with an ID that doesn't exist, they'll receive a 404 response with the message "Item not found" instead of the application crashing or returning an unexpected response.

Output when requesting /items/5:

json
{
"detail": "Item not found"
}

Adding Headers to HTTP Exceptions

Sometimes you may want to include headers in your error responses, such as for authentication errors:

python
@app.get("/protected-resource")
async def protected_resource(token: str = None):
if token != "secret-token":
raise HTTPException(
status_code=401,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return {"message": "You have access to the protected resource"}

Custom Exception Handlers

While HTTPException handles many common cases, you'll often need to create custom exceptions and handlers for specific application needs.

Creating a Custom Exception

First, let's define a custom exception:

python
class UnsupportedMediaTypeException(Exception):
def __init__(self, supported_types: list):
self.supported_types = supported_types

Registering an Exception Handler

Next, we'll create a handler for this exception and register it with FastAPI:

python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from typing import List

app = FastAPI()

class UnsupportedMediaTypeException(Exception):
def __init__(self, supported_types: List[str]):
self.supported_types = supported_types

@app.exception_handler(UnsupportedMediaTypeException)
async def unsupported_media_type_exception_handler(request: Request, exc: UnsupportedMediaTypeException):
return JSONResponse(
status_code=415,
content={
"message": "Unsupported media type",
"supported_types": exc.supported_types
},
)

@app.post("/upload/")
async def upload_file(content_type: str):
supported_types = ["image/jpeg", "image/png", "application/pdf"]
if content_type not in supported_types:
raise UnsupportedMediaTypeException(supported_types)
return {"message": "File uploaded successfully"}

When an unsupported media type is provided, the response will be:

json
{
"message": "Unsupported media type",
"supported_types": ["image/jpeg", "image/png", "application/pdf"]
}

Handling Built-in Exceptions

FastAPI allows you to override handlers for built-in exceptions to customize their responses.

Customizing RequestValidationError Responses

One common exception you might want to customize is the RequestValidationError, which occurs when request data fails validation:

python
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
price: float
quantity: int

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"status": "error",
"message": "Data validation error",
"details": jsonable_encoder(exc.errors()),
"data": jsonable_encoder(exc.body),
},
)

@app.post("/items/")
async def create_item(item: Item):
return item

With this custom handler, validation errors will be more informative:

json
{
"status": "error",
"message": "Data validation error",
"details": [
{
"loc": ["body", "price"],
"msg": "value is not a valid float",
"type": "type_error.float"
}
],
"data": {
"name": "Screwdriver",
"price": "invalid",
"quantity": 5
}
}

Overriding the Default HTTPException Handler

You can also override FastAPI's default HTTPException handler:

python
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"status": "error",
"code": exc.status_code,
"message": exc.detail,
},
)

@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id < 1:
raise HTTPException(status_code=400, detail="Item ID must be positive")
return {"item_id": item_id}

Now HTTP exceptions will follow your preferred format:

json
{
"status": "error",
"code": 400,
"message": "Item ID must be positive"
}

Real-World Example: API Error Handling System

Let's build a more comprehensive error handling system for a real-world API:

python
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from typing import Any, Dict, Optional

app = FastAPI()

class DatabaseConnectionError(Exception):
pass

class ResourceNotFoundError(Exception):
def __init__(self, resource_type: str, resource_id: Any):
self.resource_type = resource_type
self.resource_id = resource_id

class AuthorizationError(Exception):
pass

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error_code": "VALIDATION_ERROR",
"message": "Input validation error",
"details": exc.errors(),
},
)

@app.exception_handler(DatabaseConnectionError)
async def database_connection_exception_handler(request: Request, exc: DatabaseConnectionError):
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={
"error_code": "DATABASE_ERROR",
"message": "Database service unavailable, please try again later",
},
)

@app.exception_handler(ResourceNotFoundError)
async def resource_not_found_exception_handler(request: Request, exc: ResourceNotFoundError):
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"error_code": "RESOURCE_NOT_FOUND",
"message": f"{exc.resource_type} with id {exc.resource_id} not found",
},
)

@app.exception_handler(AuthorizationError)
async def authorization_exception_handler(request: Request, exc: AuthorizationError):
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"error_code": "FORBIDDEN",
"message": "You don't have permission to access this resource",
},
)

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# Log the exception here
print(f"Unhandled exception: {exc}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
},
)

# Routes that demonstrate the exceptions
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# Simulate database user lookup
if user_id < 0:
raise DatabaseConnectionError()
if user_id == 0:
raise AuthorizationError()
if user_id > 100:
raise ResourceNotFoundError("user", user_id)
return {"user_id": user_id, "name": "John Doe", "email": "[email protected]"}

This example implements a complete error handling system that:

  1. Provides consistent error response formatting
  2. Handles different types of application-specific errors
  3. Includes a global exception handler as a safety net
  4. Could easily be expanded with logging for better monitoring

Exception Handling Best Practices

Here are some best practices to follow when implementing exception handlers:

  1. Be specific: Create custom exceptions for specific error scenarios rather than using generic ones.

  2. Maintain consistency: Use a consistent response format across all error handlers.

  3. Include helpful information: Provide enough details to help API consumers understand and fix the issue.

  4. Don't expose sensitive information: Be careful not to leak sensitive details like stack traces or database credentials in error responses.

  5. Log exceptions: Always log exceptions for debugging, especially in the global exception handler.

  6. Use appropriate status codes: Choose the most appropriate HTTP status code for each error type.

  7. Consider internationalization: If your API serves a global audience, consider supporting localized error messages.

Summary

FastAPI's exception handling system gives you powerful tools to manage errors in your application:

  • Built-in HTTPException for common HTTP errors
  • Custom exception handlers for application-specific error cases
  • The ability to override default exception handlers
  • A way to create a consistent error response format

By implementing proper exception handling, you make your API more robust, easier to debug, and more user-friendly. Well-designed error responses help API consumers understand what went wrong and how to fix their requests, resulting in a better developer experience.

Additional Resources

Exercises

  1. Create a custom exception and handler for a "resource already exists" error (409 Conflict).
  2. Implement an exception handler for the 429 Too Many Requests status code to handle rate limiting.
  3. Build a more complex validation error handler that groups errors by field name.
  4. Create an exception handler that returns error responses in XML format instead of JSON.
  5. Implement a tiered exception handling system where different types of errors are logged differently based on their severity.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)