Back to Scaling Python Applications guides

FastAPI Error Handling Patterns

Stanley Ulili
Updated on February 24, 2025

Proper error handling is crucial for building secure, and maintainable APIs with FastAPI. Without thoughtful error management, web applications risk instability, unintended exposure of sensitive information, and unclear or confusing error responses for users.

This guide explores essential error-handling techniques specifically tailored for FastAPI applications.

Let's get started!

What are errors in FastAPI?

Errors in FastAPI applications are instances of Python’s built-in Exception class or its subclasses. These typically fall into three main categories:

Operational errors

Operational errors result from external factors rather than application codebase defects. They are anticipated and require proper handling to maintain the stability of your FastAPI application.

Common examples include:

  • A client requesting a resource that does not exist (404 Not Found).
  • Validation failures when users submit incorrect data (422 Unprocessable Entity).
  • Database connection issues due to network problems (500 Internal Server Error).
  • Authentication failures due to incorrect credentials (401 Unauthorized).

Programmer errors

Programmer errors originate from mistakes within the application code and typically require immediate correction instead of runtime handling.

For example, referencing undefined variable results in a NameError, while attempting to call a method on the wrong type causes a TypeError. Improper handling of asynchronous functions or exceptions in the application logic can also lead to unhandled exceptions, potentially destabilizing the application.

System errors

These errors occur at the operating system or hardware level. Though less common, unhandled system errors can crash your FastAPI application.

Examples of system errors include:

  • File system errors when reading or writing files impact application functionality.
  • Database connection interruptions due to server or network instability.
  • Memory leaks or hardware failures causing resource exhaustion and degraded performance.

With a clear understanding of these error types, the next step is to learn how to handle them effectively in FastAPI.

Working with HTTPException in FastAPI

FastAPI's HTTPException is your primary tool for communicating expected errors clearly and consistently to your API clients. Unlike standard Python exceptions, HTTPException is specifically built for web applications, allowing you to leverage standard HTTP status codes and provide clients helpful, descriptive error messages.

Here’s a basic example demonstrating how you might use HTTPException to handle a missing resource.

In this snippet, if the requested item_id isn't present in your data, FastAPI returns a clear 404 Not Found response with an informative message:

 
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/items/{item_id}")
def get_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return items[item_id]

When this exception is raised, FastAPI automatically converts it into an HTTP response with the specified status code. It includes a JSON-formatted body containing the provided detailed message, ensuring your error responses are predictable and easy to parse for clients.

Sometimes, you'll want to provide additional context to clients beyond just an error message. HTTP headers offer a structured way to pass supplementary information, making error handling easier for client applications. Here's how you can use HTTPException to include custom headers containing additional metadata:

 
@app.get("/users/{user_id}")
def get_user(user_id: int):
    if not user_exists(user_id):
        raise HTTPException(
            status_code=404,
            detail="User not found",
            headers={"X-Error-Code": "USER_NOT_FOUND"}
        )

In this example, if the specified user doesn't exist, the API returns the human-readable error message "User not found" and a header (X-Error-Code) indicating a standardized machine-readable error code. This is particularly useful when clients need consistent, stable identifiers to handle errors programmatically, separate from the descriptive error message itself.

Using headers in this way allows you to include valuable metadata, such as error codes for internal tracking, retry-after intervals for rate-limiting scenarios, or authentication details, enriching your error responses and improving your API's usability.

Implementing global error handlers in FastAPI

FastAPI provides a powerful and efficient approach to centralize error handling through exception handlers. Rather than duplicating error-handling logic within each route, you can define global handlers that consistently process exceptions across your entire application.

When working with FastAPI, the most common exception type you'll encounter is HTTPException. To ensure all HTTP errors are handled uniformly, you can define a global exception handler using FastAPI's decorator syntax. Here’s how you can set up a basic handler for HTTPException:

 
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={
            "detail": exc.detail,
            "path": request.url.path
        }
    )

In this example, the function http_exception_handler intercepts any HTTPException thrown within your routes and converts it into a structured JSON response. The response includes the error detail and the path that triggered the error, making it easier for clients to debug issues.

With the global handler in place, your route functions become simpler. They can now focus solely on business logic without explicitly formatting error responses. Here's an example of a route that raises an HTTPException, which will automatically be handled by the global handler defined above:

 
@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return items[item_id]

While handling expected HTTP exceptions covers most common error scenarios, you should also anticipate unexpected exceptions that might occur at runtime. To ensure these unexpected errors do not expose sensitive details or crash your application, you can define a more general handler for all other exceptions. This handler acts as a safety net, logging critical error details internally while providing a generic message to the client:

 
import logging

logger = logging.getLogger(__name__)

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    logger.error(f"Unexpected error: {str(exc)}")

    return JSONResponse(
        status_code=500,
        content={
            "detail": "An unexpected error occurred",
            "path": request.url.path
        }
    )

In this snippet, global_exception_handler captures any exception not explicitly handled elsewhere. It logs the error details for you to diagnose the issue later, ensuring users see a secure and straightforward message indicating an internal problem.

To further enhance the utility of your error responses, consider adding additional context, such as error codes and timestamps. These extra details can significantly improve the client-side debugging experience. Here's an example of how you might extend your global handler for HTTPException to include this richer context:

 
from datetime import datetime

@app.exception_handler(HTTPException)
async def enhanced_error_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "detail": exc.detail,
            "error_code": exc.headers.get("X-Error-Code") if exc.headers else None,
            "timestamp": datetime.utcnow().isoformat()
        }
    )

With this improved handler, you can now explicitly pass custom error codes within your route functions, making your API responses more informative. Here’s a route demonstrating how to raise an HTTPException with a custom error code:

 
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    if not user_exists(user_id):
        raise HTTPException(
            status_code=404,
            detail="User not found",
            headers={"X-Error-Code": "USER_NOT_FOUND"}
        )

Implementing global error handlers in FastAPI greatly improves your error management strategy, making your application more maintainable, secure, and user-friendly.

Creating custom exceptions in FastAPI

While FastAPI's built-in HTTPException handles many common scenarios, real-world applications often require more specialized error handling. Custom exceptions allow you to model domain-specific error cases, making your code more expressive and easier to maintain.

Consider an e-commerce application where you need to handle inventory-related errors. Instead of using generic HTTP exceptions, you can create custom exceptions that precisely describe what went wrong:

 
class InventoryException(HTTPException):
    def __init__(self, item_id: str, current_stock: int):
        super().__init__(
            status_code=400,
            detail=f"Insufficient stock for item {item_id}. Available: {current_stock}"
        )

You can use this custom exception in your routes to provide clear feedback when inventory issues occur. The error message now carries specific context about the problem, making it easier for clients to understand and handle the error appropriately:

 
@app.post("/orders/")
async def create_order(order: OrderCreate):
    if order.quantity > inventory.get_stock(order.item_id):
        raise InventoryException(order.item_id, inventory.get_stock(order.item_id))
    return await orders.create(order)

As your application grows, you might want to create a base exception class that all your custom exceptions inherit from. This approach helps organize error handling and enables consistent error formatting:

 
class ApiException(HTTPException):
    def __init__(self, status_code: int, detail: str, error_code: str):
        super().__init__(
            status_code=status_code,
            detail=detail,
            headers={"X-Error-Code": error_code}
        )

class PaymentException(ApiException):
    def __init__(self, amount: float):
        super().__init__(
            status_code=402,
            detail=f"Payment failed for amount ${amount:.2f}",
            error_code="PAYMENT_FAILED"
        )

To maintain consistent error responses, register a handler for your custom exceptions. This handler can format the error response while preserving the additional context your custom exceptions provide:

 
@app.exception_handler(ApiException)
async def api_exception_handler(request: Request, exc: ApiException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "detail": exc.detail,
            "error_code": exc.headers["X-Error-Code"],
            "timestamp": datetime.utcnow().isoformat()
        }
    )

Custom exceptions particularly shine when handling domain-specific validation. While FastAPI's built-in validation is powerful, you might need to add business-specific rules:

 
class BusinessHoursException(ApiException):
    def __init__(self):
        super().__init__(
            status_code=400,
            detail="Operation not allowed outside business hours (9 AM - 5 PM)",
            error_code="OUTSIDE_BUSINESS_HOURS"
        )

@app.post("/appointments/")
async def create_appointment(time: datetime):
    if time.hour < 9 or time.hour >= 17:
        raise BusinessHoursException()
    return await appointments.create(time)

Custom exceptions create a more expressive error-handling system that directly reflects your application's domain language. This approach makes your code easier to maintain and improves feedback for API consumers.

Always keep custom exceptions focused and ensure their error messages remain clear and actionable.

Logging FastAPI errors

Logging is essential for effectively handling errors in FastAPI applications. While structured error responses inform API users about issues, logging helps you quickly diagnose and fix problems. FastAPI integrates Python's built-in logging and external monitoring tools, allowing comprehensive error tracking.

Configure logging early, preferably during the application's startup phase, to ensure consistency across modules.

Here's a straightforward logging setup for FastAPI:

 
import logging
from fastapi import FastAPI, Request

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

app = FastAPI()

With this basic configuration, logs include critical context such as timestamps, severity levels, logger names, and descriptive messages—valuable when debugging or monitoring the application's health.

Once logging is configured, your next step is to explicitly enhance your global exception handlers to log errors. Different error scenarios warrant different logging levels.

Logging at a' WARNING' level is typically appropriate for anticipated HTTP errors (HTTPException), such as client input validation failures or resource not found errors.

Here’s an example of how you might log these expected errors, capturing useful request context such as headers and error details:

 
from fastapi.responses import JSONResponse

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    logger.warning(
        f"HTTP {exc.status_code} error during {request.method} {request.url.path}",
        extra={
            "headers": dict(request.headers),
            "detail": exc.detail
        }
    )
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})

It's essential to capture as much context as possible for unexpected errors- those that indicate bugs, exceptions, or other unforeseen circumstances.

Logging at an ERROR level along with stack traces helps you quickly pinpoint and address underlying issues.

You can achieve comprehensive error logging by implementing middleware that wraps around each request to handle unexpected exceptions gracefully:

 
@app.middleware("http")
async def request_logging_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception as exc:
        logger.error(
            "Unhandled exception encountered",
            extra={
                "path": request.url.path,
                "method": request.method,
                "client_host": request.client.host
            },
            exc_info=True  # Includes detailed stack trace
        )
        raise  # Re-raise the exception to allow FastAPI's default handlers to process it

To further strengthen your logging strategy, particularly in distributed or microservices-based environments, consider using correlation IDs.

These unique identifiers trace a request's journey through your system, enabling you to correlate logs from multiple services and components related to the same transaction.

FastAPI middleware can easily manage correlation IDs by extracting them from incoming requests or generating them when absent:

 
import uuid

@app.middleware("http")
async def correlation_id_middleware(request: Request, call_next):
    correlation_id = request.headers.get("X-Correlation-ID", str(uuid.uuid4()))
    request.state.correlation_id = correlation_id

    response = await call_next(request)
    response.headers["X-Correlation-ID"] = correlation_id
    return response

Including correlation IDs in logs significantly improves traceability. To integrate these IDs directly into your log records, you can use logging filters or structured logging libraries, ensuring every log entry clearly references its originating request.

Centralizing logs with Better Stack

Centralizing logs is important for enhancing observability and maintaining reliable FastAPI applications. Better Stack provides a great solution for centralizing FastAPI application logs, allowing you to track application behavior and promptly respond to issues efficiently.

Integrating FastAPI with Better Stack is straightforward, using Python’s standard logging system and Better Stack’s official logging handler, logtail-python. To get started, install the Logtail handler with:

 
pip install logtail-python

Then, configure FastAPI’s logging setup to route logs directly to Better Stack. Here's an example of setting up centralized logging at the start of your FastAPI application:

 
import logging
from logtail import LogtailHandler
from fastapi import FastAPI

app = FastAPI()

# Configure Better Stack logging handler
better_stack_handler = LogtailHandler(source_token="<your_better_stack_source_token>")

logging.basicConfig(
    handlers=[better_stack_handler],
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

Replace <your_better_stack_source_token> with the token your Better Stack account provided. Once configured, your logs are automatically streamed to Better Stack, where you can monitor real-time activity, filter logs based on levels, endpoints, or correlation IDs, and set up customized alerts for critical events.

Better Stack Logs Interface
~

Centralizing FastAPI logs with Better Stack enables your team to rapidly pinpoint issues, simplify debugging processes, and proactively manage application stability, resulting in a more reliable and maintainable API.

Final thoughts

Effective error handling and logging are foundational practices for building robust, maintainable, and secure FastAPI applications.

Implementing these best practices will lead to clearer, more informative client error responses and a good developer debugging process.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github