Skip to content

Error Handling Basics with FastAPI and Pydantic


🎯 What You’ll Learn

  • How FastAPI handles validation errors automatically
  • How to raise custom HTTP exceptions
  • How to structure and return meaningful error messages
  • How errors appear in the auto-generated documentation

🚨 Automatic Validation Errors

FastAPI uses Pydantic to validate request bodies, query parameters, and path parameters. If the incoming data doesn’t match the expected types or structure, FastAPI automatically returns a 422 Unprocessable Entity error.

🧪 Example: Invalid Request Body

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Task(BaseModel):
    title: str
    completed: bool

@app.post("/tasks")
def create_task(task: Task):
    return {"message": "Task created", "task": task}

🧭 Try sending this JSON:

{
  "title": 123,
  "completed": "nope"
}

FastAPI responds with:

{
  "detail": [
    {
      "loc": ["body", "title"],
      "msg": "str type expected",
      "type": "type_error.str"
    },
    {
      "loc": ["body", "completed"],
      "msg": "value could not be parsed to a boolean",
      "type": "type_error.bool"
    }
  ]
}

🔍 Breakdown of Error Response

  • loc: Location of the error (body, query, path, etc.)
  • msg: Human-readable error message
  • type: Error category (e.g., type_error.str)

These errors are:

  • Automatically generated
  • Consistent and structured
  • Visible in /docs and /redoc

🔧 Raising Custom HTTP Exceptions

FastAPI lets you raise custom errors using HTTPException.

🧪 Example: Manual Error Handling

from fastapi import HTTPException

@app.get("/divide")
def divide(x: float, y: float):
    if y == 0:
        raise HTTPException(status_code=400, detail="Division by zero is not allowed")
    return {"result": x / y}

🧭 Try:

  • /divide?x=10&y=2 → ✅ {"result": 5.0}
  • /divide?x=10&y=0 → ❌ {"detail": "Division by zero is not allowed"}

🔍 Custom Error Fields

  • status_code: Any valid HTTP status (e.g., 400, 404, 403)
  • detail: Message shown to the client
  • You can also include headers (e.g., for authentication errors)

🧪 Example: Resource Not Found

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    fake_db = {1: "Buy milk", 2: "Write code"}
    if task_id not in fake_db:
        raise HTTPException(status_code=404, detail="Task not found")
    return {"task_id": task_id, "title": fake_db[task_id]}

🧭 Try:

  • /tasks/1 → ✅ {"task_id": 1, "title": "Buy milk"}
  • /tasks/99 → ❌ {"detail": "Task not found"}

🧠 Best Practices

  • Use automatic validation for input errors
  • Use HTTPException for business logic errors
  • Always return clear, actionable error messages
  • Avoid leaking internal details (e.g., stack traces, database errors)

🧪 Practice Challenge

Create an endpoint:

  • /login that accepts a username and password via a Pydantic model
  • If the username is not "admin" or the password is not "secret", raise a 401 error
  • Return a success message otherwise