2025, Dec 30 21:00

Understanding FastAPI dependencies with yield: log order, response lifecycle, and teardown best practices

Learn why prints after yield appear post-response in FastAPI dependencies with yield. Understand the response lifecycle and manage AsyncSession teardown safely.

Why does a print placed after yield in a FastAPI dependency appear after the endpoint finishes? The short answer: with dependencies that use yield, FastAPI pauses the dependency at yield, runs your endpoint and any nested logic, generates the response, and only then resumes the dependency to execute the code after yield. That leads to logs like “After handler” showing up before “after yield”.

Minimal reproduction

The following setup demonstrates the behavior. Names are arbitrary, the logic matches a typical flow: a request hits a path operation, a dependency yields an AsyncSession, the handler runs, then FastAPI returns a response and performs cleanup after the yield point.

@router.post("/tel/verify-otp", summary="Verify OTP and update user tel number")
async def confirm_tel_otp(
        payload: UpdateTelCommand = Body(...),
        svc: TelOtpVerifier = Depends(provide_tel_otp_verifier),
):
    await svc.handle(command=payload)
    print('After handler')
    return {
        "message": tm(key="success"),
        "data": None
    }
async def provide_tel_otp_verifier(
    wu: WorkUnit = Depends(get_work_unit),
    actor = Depends(fetch_active_user)
) -> TelOtpVerifier:
    return TelOtpVerifier(uow=wu, current_user=actor)
async def open_session() -> AsyncSession:
    sess = async_session_factory()
    print('before yield')
    yield sess
    print('after yield')

async def fetch_active_user(
        token: str = Depends(oauth2_scheme),
        session: AsyncSession = Depends(open_session)
) -> Union[User, None]:
    token_hash = generate_token_hash(token)
    repo = UserRepository(session)
    usr = await repo.get_by_token(token_hash)
    if not usr:
        raise UnauthorizedError()
    return usr
class TelOtpVerifier:
    TASK_TYPE_PHONE_CONFLICT = 8
    TASK_STATUS_OPEN = 2
    CONFLICT_RESOLUTION_DAYS = 2

    def __init__(self, uow: WorkUnit, current_user: User):
        self.uow = uow
        self.current_user = current_user

    async def handle(self, command: UpdateTelCommand) -> None:
        print('start of handler')
        async with self.uow as w:
            await self._validate_otp(command, self.uow)
            ...

When the API is called, the console shows:

before yield
start of handler
After handler
after yield

What is actually happening

This is FastAPI’s dependency-with-yield lifecycle. The framework executes the dependency until yield and injects the yielded value into the path operation (and any downstream dependencies). Everything before and including yield runs before the response is created. The suspended dependency is then resumed after the response is sent, and the code following yield runs at that time. Thus the endpoint and its handler execute while the session is active, and only after the entire request completes FastAPI continues after yield to perform cleanup. This exactly explains the observed order: “before yield” prints first, then the handler logs, then the endpoint returns, and finally the code after yield prints.

Official docs cover the behavior in detail and include a canonical pattern: Dependencies with yield.

Solution in practice

No bug here; this is the expected lifecycle. The fix is conceptual: treat everything after yield as teardown that will run after the response. If you need guaranteed cleanup, place it after yield, usually in a try/finally block. A minimal pattern looks like this:

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

Applying the same idea to the earlier example keeps the log semantics intact and makes the intent explicit:

async def open_session() -> AsyncSession:
    sess = async_session_factory()
    try:
        print('before yield')
        yield sess
    finally:
        print('after yield')

Why you want this mental model

Understanding the yield boundary prevents confusion during debugging and helps structure dependencies correctly. Anything required for handling the request must happen before yield or inside the endpoint/handler. Anything that releases resources or performs cleanup must be placed after yield. With this model, log ordering, session scoping, and resource lifecycle become predictable.

Takeaways

If you see “after yield” printed after your endpoint has already logged “After handler”, it’s working as designed. The yielded value powers your path operation during execution, the response is produced, and only then FastAPI resumes the dependency to run the code that follows yield. Use try/finally around yield for safe teardown, and keep critical request-time logic before the yield point.