Main Ecosystems
HomePython CorePandas ReferenceNumPy ScientificFastAPI & PydanticDjango Enterprise
More Ecosystems
Environment & SetupRequests & HTTPAsyncIO ConcurrencyObject-Oriented OOPPyTorch Deep LearningScikit-Learn MLFlask FrameworkWeb ScrapingDatabase & ORMDevOps & Docker

FastAPI Lifespan Error: async generator function didnt yield

Verified FixPython 3.10+FastAPI 0.110+ / Starlette 0.36+Silo: fastapi

Quick Fix / Solution Rapide

Decorate your lifespan function with @asynccontextmanager and place exactly one yield statement between startup and shutdown logic.

Root Cause Analysis

This error occurs when Python initializes a FastAPI application configured with a modern lifespan context manager, but the lifespan generator function completes or returns without executing a yield statement, or yields more than once.

1. Missing @asynccontextmanager Decorator

In FastAPI 0.93+, on_event('startup') was deprecated in favor of the lifespan parameter. The lifespan function must be decorated with @contextlib.asynccontextmanager. If the decorator is omitted, FastAPI attempts to enter the raw async generator and raises RuntimeError: async generator function didn't yield.

2. Returning Early Before the Yield Statement

An unexpected return statement or early exception in startup initialization prevents the code from reaching the yield barrier.

3. Multiple Yield Statements

Async context managers must yield exactly once (separating startup from shutdown). Multiple yields raise RuntimeError: async generator function yielded more than once.

4. Async Generator vs Standard Generator Confusion

Using @contextmanager (synchronous) instead of @asynccontextmanager on an async def lifespan function.

Reproduction Code (MCVE)

Example: Bug Reproduction
import inspect

# Simulating a broken lifespan manager that lacks a yield statement
async def broken_lifespan_handler():
    print('Initializing database...')
    # Missing yield statement

def validate_lifespan(handler):
    if not inspect.isasyncgenfunction(handler) and not hasattr(handler, '__aenter__'):
        raise RuntimeError("async generator function didn't yield: lifespan context manager must yield exactly once")

validate_lifespan(broken_lifespan_handler)

Solution 1: Use @asynccontextmanager with Single yield Statement

Wrap startup and shutdown routines inside @asynccontextmanager with an explicit yield separating them.

Example: Recommended Solution
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
    # --- STARTUP LOGIC ---
    print('Lifespan: Database connection pool opened.')
    yield {'db_pool': 'connected'}
    # --- SHUTDOWN LOGIC ---
    print('Lifespan: Database connection pool closed.')

# Test entering lifespan context
import asyncio
async def test_lifespan():
    async with lifespan(None) as state:
        print(f'App running with state: {state}')

asyncio.run(test_lifespan())

Solution 2: Protect Startup Code with try...finally Block

Use try/finally inside the lifespan generator to ensure cleanup runs even if shutdown errors occur.

Example: Alternative Solution
from contextlib import asynccontextmanager

@asynccontextmanager
async def robust_lifespan(app):
    print('Resource initialized.')
    try:
        yield
    finally:
        print('Resource cleanup guaranteed in finally block.')

A common mistake is forgetting that app: FastAPI must be accepted as the first argument in async def lifespan(app: FastAPI):. Even if you don't inspect app, the parameter is required by Starlette's signature protocol. Edge cases occur when using state variables: yield a dictionary or attach variables to app.state during startup for global accessibility. Contrast this error with RuntimeError: Event loop is closed, which occurs during pytest teardown if loops are closed prematurely.