Search Topics
Search across all FastAPI topics
HTTP Exceptions
Core ConceptHTTPException is FastAPI's way of returning error responses with proper status codes. Raise it anywhere in your code to immediately stop and return an error.
Your endpoint hits an error, but instead of raising an exception, you return {'error': 'Not found'} with status 200. The frontend's error handling never triggers because the status code says 'success'. Users see broken UI and think everything is fine.
# Your endpoint:
@app.get("/items/{id}")
def get_item(id: int):
item = db.get(id)
if not item:
return {"error": "Not found"} # ← Status 200!
# Frontend:
const res = await fetch("/items/999");
if (res.ok) { // true! status is 200
const data = await res.json();
showItem(data); // Shows: {"error": "Not found"} as if it's an item
}Question
Ever returned an error message but forgot to set the right status code? Your API says "Not found" but the HTTP response says "200 OK." The frontend trusts the status code, not your message body. Broken UI, zero error handling triggered.
This is exactly why raise HTTPException exists. It sets the correct status code AND stops your code immediately. Let's see how.
raise vs return: Why It Matters
There are two ways to send an error response. One is correct, one leads to the bug we just saw.
return {"error": ...}
Status 200 (wrong!)
Frontend sees res.ok = true
Skips error handling
Broken UI
Error data shown as content
raise HTTPException(404)
Status 404 (correct!)
Frontend sees res.ok = false
Error handler runs
Clean error message
User sees helpful feedback
The Core Rule
What you just learned
raise HTTPException sets the correct status code AND stops execution
return with error data still sends status 200 — frontends won't catch it
raise is an exception — nothing after it runs in the same function
HTTP status codes are how clients decide if a request succeeded or failed
Basic HTTP Exceptions
Here's the correct way to handle a "not found" case. Import HTTPException, and raise it with a status code and detail message.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo item"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
# This stops execution and returns a proper 404
raise HTTPException(
status_code=404,
detail="Item not found",
)
# This only runs if the item exists
return {"item": items[item_id]}Insight
Notice the raise keyword. It's not return. When you raise an exception, Python immediately exits the function. The return {"item": items[item_id]} line at the bottom? It never runs. That's the safety net — you can't accidentally return data after an error.
See the Exception Flow
Watch what happens step by step when an endpoint raises HTTPException. Follow the request from arrival to JSON error response.
Exception Flow
What you just learned
HTTPException is caught by FastAPI's internal error handler
The status code and detail are turned into a JSON response automatically
No try/except needed in your endpoint — FastAPI handles it
The response body is always {"detail": "your message"}
Custom Headers on Errors
Sometimes the status code and message aren't enough. You might need to tell the client when to retry, or pass a machine-readable error code in a header.
@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",
headers={
"X-Error-Code": "ITEM_NOT_FOUND", # Machine-readable
"X-Retry-After": "30", # Hint for the client
},
)
return {"item": items[item_id]}Structured Error Details
Here's something most people don't realize: the detail field isn't limited to strings. You can pass any JSON-serializable value — dicts, lists, nested objects. This is powerful when your frontend needs structured error information.
# detail can be a dict — not just a string!
raise HTTPException(
status_code=422,
detail={
"error": "validation_failed",
"fields": [
{"field": "email", "message": "Invalid format"},
{"field": "age", "message": "Must be positive"},
],
},
)
# Response body:
# {
# "detail": {
# "error": "validation_failed",
# "fields": [
# {"field": "email", "message": "Invalid format"},
# {"field": "age", "message": "Must be positive"}
# ]
# }
# }Error Customization
What you just learned
Custom headers add machine-readable context to error responses
The detail field accepts dicts, lists, and nested structures — not just strings
Structured errors let frontends show field-level validation messages
Use X-Error-Code headers for error codes that frontends can switch on
Go Deeper: The Silent Failure Pattern
Let's look at the exact bug from our opening scenario and its fix side by side.
Returning Errors with Status 200
You return an error message as a dict, but forget to set the status code. The frontend's fetch API sees 200 OK and treats it as success.
@app.get("/items/{id}")
def get_item(id: int):
item = db.get(id)
if not item:
# Looks right, but status is 200!
return {"error": "Not found"}
return {"item": item}Think about it...
What's the difference between raise HTTPException(404) and return JSONResponse(status_code=404, content={...})?
Hint: Think about what 'raise' does in Python vs what 'return' does.
Key Points
Raise, Don't Return
Raising HTTPException sets the correct status code automatically
Rich Details
detail can be a string, dict, or list — any JSON-serializable value
Custom Headers
Add headers like Retry-After or custom error codes
Status Codes
Use fastapi.status for readable constants like HTTP_404_NOT_FOUND