Search Topics

Search across all FastAPI topics

GitHub

HTTP Exceptions

Core Concept

HTTPException 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.

terminal
# 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.

main.py
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.

main.py
@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.

main.py
# 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.

Broken code
main.py
@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