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

  1. Catch CleanupError and iterate err.exceptions to identify every root cause.
  2. Make each __aexit__ log and swallow non-fatal errors so a single failure does not cascade into CleanupError.
  3. 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

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


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)