If you have been following this FastAPI series, we have built CRUD operations, connected to a database, and secured our endpoints with JWTs. But what happens when things go wrong?
When a user searches for an item that doesn't exist, or tries to access a route without a token, your API needs to communicate the exact problem. Returning a generic 500 Internal Server Error or a standard 200 OK with an error message in the body is a bad practice.
Today, we are going to look at the most common HTTP status codes and how to raise them cleanly using FastAPI's HTTPException.
The Core Status Codes You Need to Know
HTTP status codes are grouped by their starting number.
2xx (Success): Everything worked perfectly.
4xx (Client Error): The user did something wrong (bad data, missing token, wrong URL).
5xx (Server Error): Your code broke.
Here are the four most critical 4xx errors you will use in FastAPI:
1. 404 Not Found
Use this when a requested resource does not exist in your database.
from fastapi import FastAPI, HTTPException
app = FastAPI()
fake_db = {"1": "Apple", "2": "Banana"}
@app.get("/items/{item_id}")
def get_item(item_id: str):
if item_id not in fake_db:
raise HTTPException(
status_code=404,
detail=f"Item with ID {item_id} does not exist."
)
return {"item": fake_db[item_id]}
2. 400 Bad Request
Use this when the user sends invalid data. Note: If you use Pydantic models (like we covered in Part 4), FastAPI automatically handles 422 Validation Errors for you. But you can use 400 for custom business logic errors.
@app.post("/transfer")
def transfer_funds(amount: int):
if amount <= 0:
raise HTTPException(
status_code=400,
detail="Transfer amount must be greater than zero."
)
return {"message": "Transfer successful"}
This is 422 error, when we add integers where strings should be there.
3. 401 Unauthorized (Authentication)
This is the error you throw when someone tries to access a protected route without a JWT token, or with an expired/invalid token. Think of this as: "I don't know who you are."
from fastapi import Header
@app.get("/profile")
def get_profile(authorization: str = Header(default=None)):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(
status_code=401,
detail="Missing or invalid authentication token",
headers={"WWW-Authenticate": "Bearer"},
)
return {"user": "ZeroShotAnu"}
4. 403 Forbidden (Authorization)
This is often confused with 401. You use 403 Forbidden when you know who the user is (their token is valid), but they do not have the permission to do what they are trying to do. Think of this as: "I know who you are, but you aren't an Admin."
@app.delete("/users/{user_id}")
def delete_user(user_id: str, current_user_role: str = "guest"):
# The user is logged in, but they are just a guest
if current_user_role != "admin":
raise HTTPException(
status_code=403,
detail="You do not have permission to delete users."
)
return {"message": "User deleted"}
Another example I tried here is, we already have this user existing and hence it is forbidden to create the same user again.
Error
So when I create a real new student,
it shows a success response:
Why This Matters
Proper status codes make your API predictable. When a frontend developer (or an AI Agent) interacts with your API, they don't read the text in your detail message first; their code looks at the status_code to decide what to do next.
If they see a 401, they redirect the user to the login screen.
If they see a 404, they show a "Page Not Found" illustration.
The next post will be the last post in this series, we will learn about APIRouter and Background tasks. Hope you enjoyed this series.
Do ask any questions you have in the comments below.









Top comments (0)