Module 5: Error Handling and CORS

Custom Exception Handlers, a Consistent Format, and Basic Logging

Capsule overview

In the previous capsule you learned HTTPException — the direct way to return errors from an endpoint. It works for individual cases, but as your API grows, problems show up: every endpoint defines its own error format, the client can't predict the structure, and unexpected errors return a generic 500 with no useful information.

Custom exception handlers solve this. You define exception classes for each type of business error (BookNotFoundError, DuplicateBookError) and register global handlers that intercept those exceptions and return consistently formatted responses. Every error in your API — 404, 422, 500 — has exactly the same structure.

By the end of this capsule you'll be able to create custom exceptions, register global exception handlers, define a consistent error format, customize Pydantic's 422 response, catch unexpected errors with a catch-all handler, and add basic logging.


The problem: inconsistent errors

Look at this code with HTTPException in every endpoint:

from fastapi import FastAPI, HTTPException

app = FastAPI()

books = [
    {"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez", "year": 1967},
    {"id": 2, "title": "Hopscotch", "author": "Julio Cortázar", "year": 1963},
]

@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    raise HTTPException(status_code=404, detail="Book not found")

@app.post("/books")
def create_book(title: str, author: str, year: int):
    for book in books:
        if book["title"].lower() == title.lower():
            raise HTTPException(status_code=409, detail=f"'{title}' already exists")
    new_book = {"id": len(books) + 1, "title": title, "author": author, "year": year}
    books.append(new_book)
    return new_book

GET returns {"detail": "Book not found"}. POST returns {"detail": "'Hopscotch' already exists"}. There are no error codes, no extra data, and nothing forcing new endpoints to follow the same pattern.


Custom exception classes

Define an exception class for each kind of error in your domain:

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

class DuplicateBookError(Exception):
    def __init__(self, title: str):
        self.title = title
        self.message = f"Book with title '{title}' already exists"

Each class inherits from Exception and stores the relevant information. The advantage over HTTPException: raise BookNotFoundError(book_id=42) is semantic — you know exactly what happened without reading the message.


Registering exception handlers

The classes don't do anything on their own. You have to tell FastAPI how to turn them into HTTP responses:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

class DuplicateBookError(Exception):
    def __init__(self, title: str):
        self.title = title
        self.message = f"Book with title '{title}' already exists"

@app.exception_handler(BookNotFoundError)
async def book_not_found_handler(request: Request, exc: BookNotFoundError):
    return JSONResponse(
        status_code=404,
        content={
            "status": "error",
            "error_code": "BOOK_NOT_FOUND",
            "message": exc.message,
            "detail": {"book_id": exc.book_id}
        }
    )

@app.exception_handler(DuplicateBookError)
async def duplicate_book_handler(request: Request, exc: DuplicateBookError):
    return JSONResponse(
        status_code=409,
        content={
            "status": "error",
            "error_code": "DUPLICATE_BOOK",
            "message": exc.message,
            "detail": {"title": exc.title}
        }
    )

books = [
    {"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez", "year": 1967},
    {"id": 2, "title": "Hopscotch", "author": "Julio Cortázar", "year": 1963},
]

@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    raise BookNotFoundError(book_id=book_id)

@app.post("/books")
def create_book(title: str, author: str, year: int):
    for book in books:
        if book["title"].lower() == title.lower():
            raise DuplicateBookError(title=title)
    new_book = {"id": len(books) + 1, "title": title, "author": author, "year": year}
    books.append(new_book)
    return new_book

Each handler receives request and exc (the exception instance), and returns a JSONResponse.

curl -s http://127.0.0.1:8000/books/99 | python -m json.tool

Expected output:

{
    "status": "error",
    "error_code": "BOOK_NOT_FOUND",
    "message": "Book with id 99 not found",
    "detail": {"book_id": 99}
}
curl -s -X POST "http://127.0.0.1:8000/books?title=Hopscotch&author=Cortázar&year=1963" | python -m json.tool

Expected output:

{
    "status": "error",
    "error_code": "DUPLICATE_BOOK",
    "message": "Book with title 'Hopscotch' already exists",
    "detail": {"title": "Hopscotch"}
}

The consistent error format

A standard structure for all of your error responses:

{
    "status": "error",
    "error_code": "BOOK_NOT_FOUND",
    "message": "Human-readable message",
    "detail": {}
}
FieldPurpose
statusAlways "error" — the client can tell errors from successes
error_codeA machine-readable code: BOOK_NOT_FOUND, VALIDATION_ERROR
messageA human-readable description
detailOptional extra data: IDs, fields with errors

The key piece is error_code. The HTTP status (404) gives the broad category. The error_code says exactly what happened — a 404 could be "book not found" or "author not found", and the client can tell them apart.

A helper centralizes the creation:

def create_error_response(status_code: int, error_code: str, message: str, detail=None):
    content = {"status": "error", "error_code": error_code, "message": message}
    if detail is not None:
        content["detail"] = detail
    return JSONResponse(status_code=status_code, content=content)

@app.exception_handler(BookNotFoundError)
async def book_not_found_handler(request: Request, exc: BookNotFoundError):
    return create_error_response(404, "BOOK_NOT_FOUND", exc.message, {"book_id": exc.book_id})

If tomorrow you add a timestamp field to every error, you change it in one place.


Overriding the 422 handler: Pydantic validation

When Pydantic rejects a request, FastAPI returns a 422 in a default format that doesn't follow your standard. Override the RequestValidationError handler:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel, Field

app = FastAPI()

def create_error_response(status_code: int, error_code: str, message: str, detail=None):
    content = {"status": "error", "error_code": error_code, "message": message}
    if detail is not None:
        content["detail"] = detail
    return JSONResponse(status_code=status_code, content=content)

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    errors = []
    for error in exc.errors():
        errors.append({
            "field": " -> ".join(str(loc) for loc in error["loc"]),
            "message": error["msg"],
            "type": error["type"]
        })
    return create_error_response(422, "VALIDATION_ERROR", "Request validation failed", errors)

class Book(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    author: str = Field(min_length=1, max_length=100)
    year: int = Field(ge=1450, le=2026)

@app.post("/books", status_code=201)
def create_book(book: Book):
    return book
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "", "year": 3000}' | python -m json.tool

Expected output:

{
    "status": "error",
    "error_code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "detail": [
        {"field": "body -> title", "message": "String should have at least 1 character", "type": "string_too_short"},
        {"field": "body -> author", "message": "Field required", "type": "missing"},
        {"field": "body -> year", "message": "Input should be less than or equal to 2026", "type": "less_than_equal"}
    ]
}

Catch-all handler and basic logging

What about bugs you never caught? Without a catch-all, the client gets a generic 500. With logging, you know what failed:

import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

app = FastAPI()

def create_error_response(status_code: int, error_code: str, message: str, detail=None):
    content = {"status": "error", "error_code": error_code, "message": message}
    if detail is not None:
        content["detail"] = detail
    return JSONResponse(status_code=status_code, content=content)

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

@app.exception_handler(BookNotFoundError)
async def book_not_found_handler(request: Request, exc: BookNotFoundError):
    logger.warning(f"Book not found: id={exc.book_id} | path={request.url.path}")
    return create_error_response(404, "BOOK_NOT_FOUND", exc.message, {"book_id": exc.book_id})

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    logger.error(
        f"Unexpected error on {request.method} {request.url.path}: {exc}",
        exc_info=True
    )
    return create_error_response(500, "INTERNAL_ERROR", "An unexpected error occurred")

@app.get("/books/{book_id}")
def get_book(book_id: int):
    if book_id == 99:
        raise BookNotFoundError(book_id=book_id)
    if book_id == 0:
        result = 1 / 0  # Simulates a bug
    return {"id": book_id, "title": "One Hundred Years of Solitude"}

Logging levels:

LevelWhen to use itExample
logger.info()Normal events"Book created: id=3"
logger.warning()Expected errors"Book not found: id=99"
logger.error()Unexpected errors"ZeroDivisionError on GET /books/0"
curl -s http://127.0.0.1:8000/books/0 | python -m json.tool

The client receives: {"status": "error", "error_code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}

In the uvicorn terminal:

2026-03-13 10:30:05 - __main__ - ERROR - Unexpected error on GET /books/0: division by zero
Traceback (most recent call last):
  File "main.py", line 34, in get_book
    result = 1 / 0
ZeroDivisionError: division by zero

exc_info=True includes the full traceback — without it, you'd only see the message.


Comparison: HTTPException vs custom exceptions

AspectHTTPExceptionCustom exception + handler
SetupZero — it ships with FastAPIYou define a class + a handler
FormatFixed {"detail": "..."}You control the whole format
ConsistencyDepends on each developerThe handler guarantees the structure
LoggingManual in every endpointCentralized in the handler
ReuseYou repeat status_code + detailraise BookNotFoundError(id) — clean
When to usePrototypes, simple errorsProfessional APIs, teams

Rule of thumb: if your API has 3 endpoints, HTTPException is fine. If it has 10+, custom exceptions save you a headache. You can use both in the same app.


Organizing your error handling

Group things logically in your file: (1) Exceptions — the classes, (2) Helper — create_error_response, (3) Handlers — the @app.exception_handler() functions, (4) Endpoints — the business logic. As your API grows, group exceptions by resource: BookNotFoundError and DuplicateBookError together; AuthorNotFoundError in another group. Each exception has a single purpose and stores the data its handler needs.


Connection to the project

The exception handlers you just learned carry over to the Module 6 project (the CRUD API). Your To-Do List API will have TaskNotFoundError and DuplicateTaskError, each with its handler, all returning the same format. The 422 override makes sure validation errors follow your standard. And the catch-all with logging protects you against unexpected errors.


Troubleshooting

Problem 1: The exception handler never runs — FastAPI returns a generic 500

Cause: You registered the handler for a different exception than the one you raise.

# ❌ Handler for BookNotFoundError but you raise BookError — no match
@app.exception_handler(BookNotFoundError)
async def handler(request, exc):
    return JSONResponse(status_code=404, content={"error": "not found"})

@app.get("/books/{book_id}")
def get_book(book_id: int):
    raise BookError("not found")

# ✅ The exception you raise matches the one you registered
@app.get("/books/{book_id}")
def get_book(book_id: int):
    raise BookNotFoundError(book_id=book_id)

Problem 2: The 422 override doesn't work

Cause: You imported from the wrong module.

# ❌ This does NOT catch Pydantic validation errors
from fastapi import HTTPException
@app.exception_handler(HTTPException)
async def handler(request, exc): ...

# ✅ The correct import
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_handler(request, exc): ...

Problem 3: The logs don't show up in the terminal

Cause: You never called logging.basicConfig().

# ❌ Unconfigured — the logs never print
logger = logging.getLogger(__name__)
logger.warning("test")

# ✅ Configure it at the top of the file
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
logger.warning("test")  # Shows up in the terminal

Exercises

Exercise 1: A basic exception handler (Easy)

Create BookNotFoundError with a handler that returns a 404 in the consistent format. Add a GET /books/{book_id} endpoint that searches a list and raises the exception when it finds nothing. Test it with an existing ID and a nonexistent one.

See solution
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

@app.exception_handler(BookNotFoundError)
async def book_not_found_handler(request: Request, exc: BookNotFoundError):
    return JSONResponse(
        status_code=404,
        content={"status": "error", "error_code": "BOOK_NOT_FOUND",
                 "message": exc.message, "detail": {"book_id": exc.book_id}}
    )

books = [
    {"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez"},
    {"id": 2, "title": "Hopscotch", "author": "Julio Cortázar"},
]

@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    raise BookNotFoundError(book_id=book_id)
curl -s http://127.0.0.1:8000/books/1
# → {"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez"}

curl -s http://127.0.0.1:8000/books/99
# → {"status": "error", "error_code": "BOOK_NOT_FOUND", ...}

Exercise 2: The 422 override (Easy)

Create a Book model with Pydantic (title min 1, author min 1, year ge 1450 le 2026). Override RequestValidationError to return your consistent format. Test it by sending a body with missing fields.

See solution
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel, Field

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    errors = [{"field": " -> ".join(str(loc) for loc in e["loc"]),
               "message": e["msg"], "type": e["type"]} for e in exc.errors()]
    return JSONResponse(
        status_code=422,
        content={"status": "error", "error_code": "VALIDATION_ERROR",
                 "message": "Request validation failed", "detail": errors}
    )

class Book(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    author: str = Field(min_length=1, max_length=100)
    year: int = Field(ge=1450, le=2026)

@app.post("/books", status_code=201)
def create_book(book: Book):
    return book
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"year": 3000}'
# → {"status": "error", "error_code": "VALIDATION_ERROR", "detail": [
#      {"field": "body -> title", "message": "Field required", ...},
#      {"field": "body -> author", "message": "Field required", ...},
#      {"field": "body -> year", "message": "Input should be less than or equal to 2026", ...}]}

Exercise 3: Multiple exceptions with a helper (Medium)

Create BookNotFoundError, DuplicateBookError, and InvalidYearError. Use a create_error_response helper. Endpoints: GET /books/{book_id} and POST /books (validating duplicate titles and future years). Test all three scenarios.

See solution
from datetime import datetime
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

app = FastAPI()

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

class DuplicateBookError(Exception):
    def __init__(self, title: str):
        self.title = title
        self.message = f"Book '{title}' already exists"

class InvalidYearError(Exception):
    def __init__(self, year: int):
        self.year = year
        self.message = f"Year {year} is in the future"

def create_error_response(status_code: int, error_code: str, message: str, detail=None):
    content = {"status": "error", "error_code": error_code, "message": message}
    if detail:
        content["detail"] = detail
    return JSONResponse(status_code=status_code, content=content)

@app.exception_handler(BookNotFoundError)
async def handle_not_found(request: Request, exc: BookNotFoundError):
    return create_error_response(404, "BOOK_NOT_FOUND", exc.message, {"book_id": exc.book_id})

@app.exception_handler(DuplicateBookError)
async def handle_duplicate(request: Request, exc: DuplicateBookError):
    return create_error_response(409, "DUPLICATE_BOOK", exc.message, {"title": exc.title})

@app.exception_handler(InvalidYearError)
async def handle_invalid_year(request: Request, exc: InvalidYearError):
    return create_error_response(400, "INVALID_YEAR", exc.message, {"year": exc.year})

class BookCreate(BaseModel):
    title: str = Field(min_length=1)
    author: str = Field(min_length=1)
    year: int

books = [{"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez", "year": 1967}]
next_id = 2

@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    raise BookNotFoundError(book_id=book_id)

@app.post("/books", status_code=201)
def create_book(book: BookCreate):
    global next_id
    if book.year > datetime.now().year:
        raise InvalidYearError(year=book.year)
    for existing in books:
        if existing["title"].lower() == book.title.lower():
            raise DuplicateBookError(title=book.title)
    new_book = {"id": next_id, **book.model_dump()}
    books.append(new_book)
    next_id += 1
    return new_book
curl -s http://127.0.0.1:8000/books/99
# → 404, error_code: "BOOK_NOT_FOUND"

curl -s -X POST http://127.0.0.1:8000/books -H "Content-Type: application/json" \
  -d '{"title": "One Hundred Years of Solitude", "author": "Other", "year": 2020}'
# → 409, error_code: "DUPLICATE_BOOK"

curl -s -X POST http://127.0.0.1:8000/books -H "Content-Type: application/json" \
  -d '{"title": "Future", "author": "Author", "year": 2099}'
# → 400, error_code: "INVALID_YEAR"

Exercise 4: A catch-all with logging (Medium)

Add a catch-all handler for Exception using logger.error() and exc_info=True. Create a GET /books/{book_id}/summary endpoint that reads a key that doesn't exist in a dict (simulating a bug). Verify that the client gets the consistent format and that the terminal shows the traceback.

See solution
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

app = FastAPI()

@app.exception_handler(Exception)
async def general_handler(request: Request, exc: Exception):
    logger.error(f"Error on {request.method} {request.url.path}: {exc}", exc_info=True)
    return JSONResponse(
        status_code=500,
        content={"status": "error", "error_code": "INTERNAL_ERROR",
                 "message": "An unexpected error occurred"}
    )

books = [{"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez"}]

@app.get("/books/{book_id}/summary")
def get_book_summary(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return {"summary": book["summary"]}  # KeyError: "summary" doesn't exist
    return {"error": "not found"}
curl -s http://127.0.0.1:8000/books/1/summary
# → {"status": "error", "error_code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}
# Terminal: ERROR - Error on GET /books/1/summary: 'summary' + the full traceback

Exercise 5: A complete error handling system (Hard)

Build an API with: a BookCreate model (Pydantic: title, author, year, isbn min 10 max 13), the exceptions BookNotFoundError and DuplicateISBNError, the 422 override, a catch-all with logging, and a helper. Endpoints: GET /books, GET /books/{book_id}, POST /books (validating duplicate ISBNs), DELETE /books/{book_id}. Test 4 error scenarios.

See solution
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel, Field

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
app = FastAPI()

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id
        self.message = f"Book with id {book_id} not found"

class DuplicateISBNError(Exception):
    def __init__(self, isbn: str):
        self.isbn = isbn
        self.message = f"ISBN '{isbn}' already exists"

def create_error_response(status_code, error_code, message, detail=None):
    content = {"status": "error", "error_code": error_code, "message": message}
    if detail is not None:
        content["detail"] = detail
    return JSONResponse(status_code=status_code, content=content)

@app.exception_handler(BookNotFoundError)
async def handle_not_found(request: Request, exc: BookNotFoundError):
    return create_error_response(404, "BOOK_NOT_FOUND", exc.message, {"book_id": exc.book_id})

@app.exception_handler(DuplicateISBNError)
async def handle_duplicate(request: Request, exc: DuplicateISBNError):
    return create_error_response(409, "DUPLICATE_ISBN", exc.message, {"isbn": exc.isbn})

@app.exception_handler(RequestValidationError)
async def handle_validation(request: Request, exc: RequestValidationError):
    errors = [{"field": " -> ".join(str(l) for l in e["loc"]),
               "message": e["msg"], "type": e["type"]} for e in exc.errors()]
    return create_error_response(422, "VALIDATION_ERROR", "Validation failed", errors)

@app.exception_handler(Exception)
async def handle_general(request: Request, exc: Exception):
    logger.error(f"Unexpected: {exc}", exc_info=True)
    return create_error_response(500, "INTERNAL_ERROR", "An unexpected error occurred")

class BookCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    author: str = Field(min_length=1, max_length=100)
    year: int = Field(ge=1450, le=2026)
    isbn: str = Field(min_length=10, max_length=13)

books = [{"id": 1, "title": "One Hundred Years of Solitude", "author": "García Márquez",
          "year": 1967, "isbn": "9780060883287"}]
next_id = 2

@app.get("/books")
def list_books():
    return books

@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    raise BookNotFoundError(book_id=book_id)

@app.post("/books", status_code=201)
def create_book(book: BookCreate):
    global next_id
    for existing in books:
        if existing["isbn"] == book.isbn:
            raise DuplicateISBNError(isbn=book.isbn)
    new_book = {"id": next_id, **book.model_dump()}
    books.append(new_book)
    next_id += 1
    return new_book

@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    for i, book in enumerate(books):
        if book["id"] == book_id:
            deleted = books.pop(i)
            return {"message": f"Book '{deleted['title']}' deleted"}
    raise BookNotFoundError(book_id=book_id)
curl -s http://127.0.0.1:8000/books/99         # → 404 BOOK_NOT_FOUND
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title":"X","author":"Y","year":2020,"isbn":"9780060883287"}'  # → 409 DUPLICATE_ISBN
curl -s -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" -d '{"title":""}'              # → 422 VALIDATION_ERROR
curl -s -X DELETE http://127.0.0.1:8000/books/99                     # → 404 BOOK_NOT_FOUND

Summary

  • Custom exceptions (BookNotFoundError, DuplicateBookError) inherit from Exception and store data relevant to the error
  • @app.exception_handler() registers a global handler that intercepts an exception and returns a JSONResponse in your format
  • A consistent format with status, error_code, message, and detail — every error follows the same structure
  • The create_error_response helper centralizes the creation of error responses
  • The 422 override with RequestValidationError (from fastapi.exceptions) customizes Pydantic's validation errors
  • A catch-all handler for Exception catches unexpected errors and returns a 500 without exposing internal details
  • Logginglogger.warning() for expected errors, logger.error() with exc_info=True for unexpected errors with a traceback
  • HTTPException vs custom exceptions: HTTPException for prototypes; custom exceptions for consistency and centralized logging

Next capsule: CORS and Middleware — you'll learn what the same-origin policy is, why CORS exists, how to configure CORSMiddleware in FastAPI, and the middleware concept.


Additional resources

  1. FastAPI - Handling Errors - Official documentation on error handling in FastAPI
  2. FastAPI - Custom Exception Handlers - Registering handlers for custom exceptions
  3. FastAPI - Override Request Validation Exceptions - Customizing the 422 response
  4. Python Logging HOWTO - The official guide to Python's logging module
  5. Starlette - Exception Handlers - Exception handlers in Starlette (FastAPI's foundation)
  6. FastAPI Best Practices - Error Handling - Recommended patterns for error handling