Yield Dependencies

advanced fastapi dependency-injection

A regular dependency returns a value. A yield dependency yields a value, runs the handler, then resumes after the yield to clean up. Think of it like a context manager (with block) wrapped around the request.

In simple language — anywhere we’d write with session() as s: in a script, we use a yield dependency in FastAPI. It guarantees teardown — close the DB session, release the lock, log the timing — no matter what happens in the handler.

The shape

from fastapi import Depends

def get_db():
    db = SessionLocal()
    try:
        yield db          # <-- value injected into handler
    finally:
        db.close()        # <-- runs AFTER the response is built

The yield splits the function in two:

  • Code before yield runs on request start (setup).
  • The value after yield is injected.
  • Code after yield runs on request end (teardown), even if the handler raised.

Lifecycle in pictures

Yield dependency lifecycle
1. Setup — before yield
open db session, start timer, acquire lock
2. yield value — FastAPI suspends here
the yielded value is injected as the param
3. Handler runs — uses the value
may raise an exception
4. Teardown — after yield
close session, release lock — ALWAYS runs (use finally)

The canonical use case — SQLAlchemy session

This is in every FastAPI codebase that talks to a DB:

from sqlalchemy.orm import Session, sessionmaker

SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(404)
    return user

Every request gets its own session. The finally block closes it — whether the handler returned cleanly, raised HTTPException, or blew up with an unexpected error.

Important: always put cleanup in finally, not after the bare yield. If we skip finally, an exception in the handler skips the cleanup.

Exception handling INSIDE the yield

We can catch exceptions raised in the handler and react before re-raising (or instead of re-raising). The pattern — wrap the yield itself.

def db_session_with_rollback():
    db = SessionLocal()
    try:
        yield db
        db.commit()                  # success path
    except Exception:
        db.rollback()                # something went wrong in the handler
        raise                        # let FastAPI's error handlers see it
    finally:
        db.close()

In simple language — yield propagates handler exceptions back into our dependency. We can catch them, do something (rollback, log), and either swallow or re-raise. If we swallow it, FastAPI thinks the handler succeeded — usually not what we want, so almost always raise.

Stacking yield dependencies

Multiple yield deps work like nested context managers. Setup happens outermost-in, teardown happens innermost-out.

def get_logger(request: Request):
    log = make_logger(request_id=request.headers.get("x-request-id"))
    log.info("request start")
    try:
        yield log
    finally:
        log.info("request end")

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post("/orders")
def create_order(
    order: OrderIn,
    db: Session = Depends(get_db),
    log = Depends(get_logger),
):
    log.info("creating order")
    return repository.save(db, order)
Stacked teardown order (LIFO)
Setup — top-down
get_logger setup
get_db setup
handler runs
Teardown — bottom-up
get_db close
get_logger close

Yield + raise after teardown? Not allowed

We can’t raise HTTPException from the teardown side (after yield). By that point the response is already constructed and being sent. If we need to abort, do it in the handler or in the setup (before yield).

Async yield works the same

For async code, use async def:

async def get_db():
    async with AsyncSessionLocal() as db:
        yield db

Or manual:

async def get_redis():
    r = await aioredis.create_redis_pool(...)
    try:
        yield r
    finally:
        r.close()
        await r.wait_closed()

Combining with Depends caching

Same per-request caching applies. If three places in the request use Depends(get_db), the session is opened once and torn down once at the end. We don’t get three sessions just because three places asked for one.

The takeaway — yield dependencies are FastAPI’s answer to “I need setup AND teardown around the request”. Always put cleanup in finally. Catch exceptions in the dep if we need to react (rollback). Stack them freely — teardown order is LIFO, exactly like nested context managers.