Home
Python
FastAPI: Exception Handling
Daniel Nguyen
Daniel Nguyen
September 17, 2026
2 min

Building a production-ready API is not only about handling successful requests. A good API should also handle failures consistently, safely, and predictably.

The key principle is simple:

Services should raise meaningful exceptions. Global handlers should convert those exceptions into consistent HTTP responses.

1. Create a Custom AppException

Start with a base exception for application-specific errors.

from typing import Any
class AppException(Exception):
def __init__(
self,
error_code: int,
error_message: str,
error: Any = None,
status_code: int = 500,
):
self.status_code = status_code
self.error_code = error_code
self.error_message = error_message
self.error = error
super().__init__(error_message)

Each exception carries four pieces of information:

  • status_code: HTTP status code returned to the client.
  • error_code: Application-specific error identifier.
  • error_message: Human-readable description.
  • error: Optional additional details, such as validation issues.

This separates application error information from the HTTP response format.

2. Create a Standard Error Response

A consistent response structure makes errors easier for frontend applications to handle.

from typing import Any
from fastapi.responses import JSONResponse
def generate_error_response(
status_code: int,
error_code: int,
message: str,
error: Any = None,
) -> JSONResponse:
return JSONResponse(
status_code=status_code,
content={
"status_code": status_code,
"error_code": error_code,
"message": message,
"error": error,
},
)

Example response:

{
"status_code": 404,
"error_code": 1001,
"message": "Client not found",
"error": null
}

The HTTP status communicates the type of failure, while error_code identifies the specific application error.

3. Implement Global Exception Handlers

Instead of repeating error-handling logic in every route, register handlers centrally.

Handle AppException

import logging
from fastapi import Request
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
async def app_exception_handler(
request: Request,
exc: AppException,
) -> JSONResponse:
if exc.status_code >= 500:
logger.error(
"Application error: %s",
exc.error_message,
)
else:
logger.warning(
"Application error: %s",
exc.error_message,
)
return generate_error_response(
status_code=exc.status_code,
error_code=exc.error_code,
message=exc.error_message,
error=exc.error,
)

This handler converts application exceptions into the standard response format.

Handle HTTPException

FastAPI’s HTTPException represents standard HTTP errors, such as 404 Not Found or 403 Forbidden.

from fastapi import HTTPException
async def http_exception_handler(
request: Request,
exc: HTTPException,
) -> JSONResponse:
return generate_error_response(
status_code=exc.status_code,
error_code=exc.status_code,
message="HTTP Exception",
error=exc.detail,
)

For production APIs, consider preserving a safe, human-readable exc.detail as the message instead of always returning "HTTP Exception".

Handle Unexpected Exceptions

Unexpected errors should be logged internally without exposing stack traces or sensitive details to clients.

async def generic_exception_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
logger.exception("Unhandled exception")
return generate_error_response(
status_code=500,
error_code=5000,
message="Internal server error",
)

The server logs retain diagnostic information, while the client receives a generic error message.

Handle Request Validation Errors

FastAPI raises RequestValidationError when incoming request data does not satisfy the declared schema.

from fastapi.exceptions import RequestValidationError
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
return generate_error_response(
status_code=422,
error_code=4220,
message="Request validation failed",
error=exc.errors(),
)

Before returning validation details, ensure they are JSON-serializable and do not expose sensitive input values.

4. Register the Handlers

Register the exception handlers when initializing the FastAPI application.

from fastapi import FastAPI
app = FastAPI()
app.add_exception_handler(
AppException,
app_exception_handler,
)
app.add_exception_handler(
HTTPException,
http_exception_handler,
)
app.add_exception_handler(
RequestValidationError,
validation_exception_handler,
)
app.add_exception_handler(
Exception,
generic_exception_handler,
)

FastAPI will dispatch matching exceptions to their registered handlers. More specific handlers should be registered for exceptions that require specialized behavior.

5. Organize Exceptions by Domain

As the application grows, create specialized exceptions for different modules.

AppException
├── DBException
├── AuthException
├── PaymentException
└── ExternalAPIException

For example:

class DBException(AppException):
pass
class AuthException(AppException):
pass

These subclasses inherit the common response metadata while making errors easier to identify and handle.

6. Raise Exceptions in the Service Layer

The service layer should focus on business logic rather than constructing HTTP responses.

from sqlalchemy.exc import SQLAlchemyError
async def get_client(session, client_id: int):
try:
result = await session.execute(
select(Client).where(Client.id == client_id)
)
client = result.scalar_one_or_none()
except SQLAlchemyError as exc:
await session.rollback()
raise DBException(
error_code=1001,
error_message="Failed to retrieve client.",
status_code=500,
) from exc
if client is None:
raise AppException(
error_code=1002,
error_message="Client not found.",
status_code=404,
)
return client

Here, database failures and missing clients are handled differently:

  • Database errors become a server error.
  • A missing client becomes a 404 response.
  • The global handler determines the response format.

The from exc clause preserves the original exception as the cause for internal debugging. Avoid returning raw database exception messages to clients.

Key Takeaways

  • Custom exceptions represent application-specific failures.
  • Global exception handlers centralize HTTP response formatting.
  • Domain-specific exceptions organize errors by responsibility.
  • Services raise exceptions instead of constructing HTTP responses.
  • Logging captures diagnostic information without exposing internal details to clients.

A clean exception-handling architecture keeps business logic independent of HTTP concerns and makes API failures consistent, secure, and easier to debug.


Tags

#Python#FastAPI

Share

Daniel Nguyen

Daniel Nguyen

Frontend Developer

Frontend developer specializing in React, Next.js, and JavaScript. Writing practical guides on modern web development at Dev98.

Expertise

React
Next.js
JavaScript
TypeScript
Python

Social Media

githublinkedinyoutubewebsite

Related Posts

AI
Building an AI Gateway with FastAPI
October 06, 2026
2 min
Dev98

Dev98

React · Next.js · Web development