aio-libs/aiohttp · error · CleanupError
Multiple errors on cleanup stage
Error message
Multiple errors on cleanup stage
What it means
During application cleanup, aiohttp runs every registered async-context-manager exit (__aexit__) in reverse registration order. If more than one exit raises, the individual exceptions are collected and re-raised as a CleanupError (a RuntimeError subclass) whose .exceptions property holds the full list. This preserves secondary failures rather than silently dropping them.
Solutions
- Catch CleanupError and iterate err.exceptions to identify every root cause.
- Make each __aexit__ log and swallow non-fatal errors so a single failure does not cascade into CleanupError.
- Order teardown deliberately — close consumers before their producers / pools.
Example fix
# before
try:
await runner.cleanup()
except Exception:
pass # masks all failures
# after
try:
await runner.cleanup()
except CleanupError as e:
for exc in e.exceptions:
logger.error('cleanup failed: %r', exc) Defensive patterns
Strategy: try-catch
Try / catch
try:
await runner.cleanup()
except CleanupError as e:
for exc in e.exceptions:
logger.error('cleanup failure: %r', exc) Prevention
- Make each cleanup context manager's __aexit__ log-and-swallow non-fatal errors.
- Order teardown: close consumers before producers, then pools.
- Register a single on_cleanup handler that aggregates and logs, rather than relying on the default.
When it happens
Trigger: Two or more cleanup_ctx handlers / async context managers each raise during shutdown; e.g. a DB pool close fails AND a background task cancellation raises AND a redis client times out.
Common situations: Shutdown under load where several resource pools fail to close; a signal/timeout tearing down connections mid-teardown; one failing cleanup masking others in naive try/except.
Related errors
- Site is not registered in runner
- Already started
- Call .prepare() first
- Call runner.setup() before making a site
- Cannot add frozen application
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/7517e0be9de55f73.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_app.py:447
if not isinstance(ctx, AbstractAsyncContextManager):
ctx = asynccontextmanager(cb)(app) # type: ignore[arg-type]
await ctx.__aenter__()
self._exits.append(ctx)
async def _on_cleanup(self, app: Application) -> None:
errors = []
for it in reversed(self._exits):
try:
await it.__aexit__(None, None, None)
except (Exception, asyncio.CancelledError) as exc:
errors.append(exc)
if errors:
if len(errors) == 1:
raise errors[0]
else:
raise CleanupError("Multiple errors on cleanup stage", errors)
View on GitHub (pinned to d041d4d0fd)