Chapter 6: Error Handling
Welcome back! In Chapter 5: Dependency Injection, we learned how to structure our code using dependencies to manage common tasks like pagination or database sessions. This helps keep our code clean and reusable.
But what happens when things don’t go as planned? A user might request data that doesn’t exist, or they might send invalid input. Our API needs a way to gracefully handle these situations and inform the client about what went wrong.
Our Goal Today: Learn how FastAPI helps us manage errors effectively, both for problems we expect (like “item not found”) and for unexpected issues like invalid input data.
What Problem Does This Solve?
Imagine our online store API. We have an endpoint like /items/{item_id}
to fetch details about a specific item. What should happen if a user tries to access /items/9999
but there’s no item with ID 9999 in our database?
If we don’t handle this, our application might crash or return a confusing, generic server error (like 500 Internal Server Error
). This isn’t helpful for the person using our API. They need clear feedback: “The item you asked for doesn’t exist.”
Similarly, if a user tries to create an item (POST /items/
) but forgets to include the required price
field in the JSON body, we shouldn’t just crash. We need to tell them, “You forgot the price field!”
FastAPI provides a structured way to handle these different types of errors, ensuring clear communication with the client. Think of it as setting up clear emergency procedures for your API.
Key Concepts
HTTPException
for Expected Errors:- These are errors you anticipate might occur based on the client’s request, like requesting a non-existent resource or lacking permissions.
- You can raise
HTTPException
directly in your code. - You specify an appropriate HTTP status code (like
404 Not Found
,403 Forbidden
) and a helpful detail message (like"Item not found"
). - FastAPI catches this exception and automatically sends a properly formatted JSON error response to the client.
RequestValidationError
for Invalid Input:- This error occurs when the data sent by the client in the request (path parameters, query parameters, or request body) fails the validation rules defined by your type hints and Pydantic models (as seen in Chapter 2: Path Operations & Parameter Declaration and Chapter 3: Data Validation & Serialization (Pydantic)).
- FastAPI automatically catches these validation errors.
- It sends back a
422 Unprocessable Entity
response containing detailed information about which fields were invalid and why. You usually don’t need to write extra code for this!
- Custom Exception Handlers:
- For more advanced scenarios, you can define your own functions to handle specific types of exceptions (either built-in Python exceptions or custom ones you create).
- This gives you full control over how errors are logged and what response is sent back to the client.
Using HTTPException
for Expected Errors
Let’s solve our “item not found” problem using HTTPException
.
-
Import
HTTPException
:# main.py or your router file from fastapi import FastAPI, HTTPException app = FastAPI() # Or use your APIRouter # Simple in-memory storage (like from Chapter 4) fake_items_db = {1: {"name": "Foo"}, 2: {"name": "Bar"}}
Explanation: We import
HTTPException
directly fromfastapi
. -
Check and Raise in Your Path Operation:
@app.get("/items/{item_id}") async def read_item(item_id: int): # Check if the requested item_id exists in our "database" if item_id not in fake_items_db: # If not found, raise HTTPException! raise HTTPException(status_code=404, detail="Item not found") # If found, proceed normally return {"item": fake_items_db[item_id]}
Explanation:
- Inside
read_item
, we check if theitem_id
exists as a key in ourfake_items_db
dictionary. - If
item_id
is not found, weraise HTTPException(...)
.status_code=404
: We use the standard HTTP status code404 Not Found
. FastAPI knows many common status codes (you can also usefrom starlette import status; raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, ...)
for more readability).detail="Item not found"
: We provide a human-readable message explaining the error. This will be sent back to the client in the JSON response body.
- If the item is found, the
raise
statement is skipped, and the function returns the item details as usual.
- Inside
How it Behaves:
- Request: Client sends
GET /items/1
- Response (Status Code 200):
{"item": {"name": "Foo"}}
- Response (Status Code 200):
- Request: Client sends
GET /items/99
- Response (Status Code 404):
{"detail": "Item not found"}
- Response (Status Code 404):
FastAPI automatically catches the HTTPException
you raised and sends the correct HTTP status code along with the detail
message formatted as JSON.
Automatic Handling of RequestValidationError
You’ve already seen this in action without realizing it! When you define Pydantic models for your request bodies or use type hints for path/query parameters, FastAPI automatically validates incoming data.
Let’s revisit the create_item
example from Chapter 3: Data Validation & Serialization (Pydantic):
# main.py or your router file
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Pydantic model requiring name and price
class Item(BaseModel):
name: str
price: float
description: str | None = None
@app.post("/items/")
# Expects request body matching the Item model
async def create_item(item: Item):
# If execution reaches here, validation PASSED automatically.
return {"message": "Item received!", "item_data": item.model_dump()}
How it Behaves (Automatically):
- Request: Client sends
POST /items/
with a valid JSON body:{ "name": "Gadget", "price": 19.95 }
- Response (Status Code 200):
{ "message": "Item received!", "item_data": { "name": "Gadget", "price": 19.95, "description": null } }
- Response (Status Code 200):
- Request: Client sends
POST /items/
with an invalid JSON body (missingprice
):{ "name": "Widget" }
- Response (Status Code 422): FastAPI automatically intercepts this before
create_item
runs and sends:{ "detail": [ { "type": "missing", "loc": [ "body", "price" ], "msg": "Field required", "input": { "name": "Widget" }, "url": "..." // Link to Pydantic error docs } ] }
- Response (Status Code 422): FastAPI automatically intercepts this before
- Request: Client sends
POST /items/
with an invalid JSON body (wrong type forprice
):{ "name": "Doohickey", "price": "cheap" }
- Response (Status Code 422): FastAPI automatically sends:
{ "detail": [ { "type": "float_parsing", "loc": [ "body", "price" ], "msg": "Input should be a valid number, unable to parse string as a number", "input": "cheap", "url": "..." } ] }
- Response (Status Code 422): FastAPI automatically sends:
Notice that we didn’t write any try...except
blocks or if
statements in create_item
to handle these validation issues. FastAPI and Pydantic take care of it, providing detailed error messages that tell the client exactly what went wrong and where (loc
). This is a huge time saver!
Custom Exception Handlers (A Quick Look)
Sometimes, you might want to handle specific errors in a unique way. Maybe you want to log a particular error to a monitoring service, or perhaps you need to return error responses in a completely custom format different from FastAPI’s default.
FastAPI allows you to register exception handlers using the @app.exception_handler()
decorator.
Example: Imagine you have a custom error UnicornNotFound
and want to return a 418 I'm a teapot
status code when it occurs.
-
Define the Custom Exception:
# Can be in your main file or a separate exceptions.py class UnicornNotFound(Exception): def __init__(self, name: str): self.name = name
-
Define the Handler Function:
# main.py from fastapi import FastAPI, Request from fastapi.responses import JSONResponse # Assuming UnicornNotFound is defined above or imported app = FastAPI() # Decorator registers this function to handle UnicornNotFound errors @app.exception_handler(UnicornNotFound) async def unicorn_exception_handler(request: Request, exc: UnicornNotFound): # This function runs whenever UnicornNotFound is raised return JSONResponse( status_code=418, # I'm a teapot! content={"message": f"Oops! Can't find unicorn named: {exc.name}."}, )
Explanation:
@app.exception_handler(UnicornNotFound)
: This tells FastAPI that theunicorn_exception_handler
function should be called whenever an error of typeUnicornNotFound
is raised and not caught elsewhere.- The handler function receives the
request
object and the exception instance (exc
). - It returns a
JSONResponse
with the desired status code (418) and a custom content dictionary.
-
Raise the Custom Exception in a Path Operation:
@app.get("/unicorns/{name}") async def read_unicorn(name: str): if name == "yolo": # Raise our custom exception raise UnicornNotFound(name=name) return {"unicorn_name": name, "message": "Unicorn exists!"}
How it Behaves:
- Request:
GET /unicorns/sparklehoof
- Response (Status Code 200):
{"unicorn_name": "sparklehoof", "message": "Unicorn exists!"}
- Response (Status Code 200):
- Request:
GET /unicorns/yolo
- Response (Status Code 418): (Handled by
unicorn_exception_handler
){"message": "Oops! Can't find unicorn named: yolo."}
- Response (Status Code 418): (Handled by
Custom handlers provide flexibility, but for most common API errors, HTTPException
and the automatic RequestValidationError
handling are sufficient.
How it Works Under the Hood (Simplified)
When an error occurs during a request, FastAPI follows a process to decide how to respond:
Scenario 1: Raising HTTPException
- Raise: Your path operation code (e.g.,
read_item
) executesraise HTTPException(status_code=404, detail="Item not found")
. - Catch: FastAPI’s internal request/response cycle catches this specific
HTTPException
. - Find Handler: FastAPI checks if there’s a custom handler registered for
HTTPException
. If not (which is usually the case unless you override it), it uses its default handler forHTTPException
. - Default Handler Executes: The default handler (
fastapi.exception_handlers.http_exception_handler
) takes thestatus_code
anddetail
from the exception you raised. - Create Response: It creates a
starlette.responses.JSONResponse
containing{"detail": exc.detail}
and sets the status code toexc.status_code
. - Send Response: This JSON response is sent back to the client.
sequenceDiagram
participant Client
participant FastAPIApp as FastAPI App
participant RouteHandler as Route Handler (read_item)
participant DefaultHTTPExceptionHandler as Default HTTPException Handler
Client->>+FastAPIApp: GET /items/99
FastAPIApp->>+RouteHandler: Call read_item(item_id=99)
RouteHandler->>RouteHandler: Check DB: item 99 not found
RouteHandler-->>-FastAPIApp: raise HTTPException(404, "Item not found")
Note over FastAPIApp: Catches HTTPException
FastAPIApp->>+DefaultHTTPExceptionHandler: Handle the exception instance
DefaultHTTPExceptionHandler->>DefaultHTTPExceptionHandler: Extract status_code=404, detail="Item not found"
DefaultHTTPExceptionHandler-->>-FastAPIApp: Return JSONResponse(status=404, content={"detail": "..."})
FastAPIApp-->>-Client: Send 404 JSON Response
Scenario 2: Automatic RequestValidationError
- Request: Client sends
POST /items/
with invalid data (e.g., missingprice
). - Parameter/Body Parsing: FastAPI tries to parse the request body and validate it against the
Item
Pydantic model before callingcreate_item
. - Pydantic Raises: Pydantic’s validation fails and raises a
pydantic.ValidationError
. - FastAPI Wraps: FastAPI catches the
pydantic.ValidationError
and wraps it inside its ownfastapi.exceptions.RequestValidationError
to add context. - Catch: FastAPI’s internal request/response cycle catches the
RequestValidationError
. - Find Handler: FastAPI looks for a handler for
RequestValidationError
and finds its default one. - Default Handler Executes: The default handler (
fastapi.exception_handlers.request_validation_exception_handler
) takes theRequestValidationError
. - Extract & Format Errors: It calls the
.errors()
method on the exception to get the list of validation errors provided by Pydantic. It then formats this list into the standard structure (withloc
,msg
,type
). - Create Response: It creates a
JSONResponse
with status code422
and the formatted error details as the content. - Send Response: This 422 JSON response is sent back to the client. Your
create_item
function was never even called.
Code Connections
fastapi.exceptions.HTTPException
: The class you import and raise for expected client errors. Defined infastapi/exceptions.py
. It inherits fromstarlette.exceptions.HTTPException
.fastapi.exception_handlers.http_exception_handler
: The default function that handlesHTTPException
. Defined infastapi/exception_handlers.py
. It creates aJSONResponse
.fastapi.exceptions.RequestValidationError
: The exception FastAPI raises internally when Pydantic validation fails for request data. Defined infastapi/exceptions.py
.fastapi.exception_handlers.request_validation_exception_handler
: The default function that handlesRequestValidationError
. Defined infastapi/exception_handlers.py
. It callsjsonable_encoder(exc.errors())
and creates a 422JSONResponse
.@app.exception_handler(ExceptionType)
: The decorator used on theFastAPI
app instance to register your own custom handler functions. Theexception_handler
method is part of theFastAPI
class infastapi/applications.py
.
Conclusion
You’ve learned how FastAPI helps you manage errors gracefully!
- You can handle expected client errors (like “not found”) by raising
HTTPException
with a specificstatus_code
anddetail
message. - FastAPI automatically handles validation errors (
RequestValidationError
) when incoming data doesn’t match your Pydantic models or type hints, returning detailed422
responses. - You can define custom exception handlers for fine-grained control over error responses and logging using
@app.exception_handler()
.
Using these tools makes your API more robust, predictable, and easier for clients to interact with, even when things go wrong. Clear error messages are a crucial part of a good API design.
Now that we know how to handle errors, let’s think about another critical aspect: security. How do we protect our endpoints, ensuring only authorized users can access certain data or perform specific actions?
Ready to secure your API? Let’s move on to Chapter 7: Security Utilities!
Generated by AI Codebase Knowledge Builder