{"id":"7517e0be9de55f73","repo":"aio-libs/aiohttp","slug":"multiple-errors-on-cleanup-stage","errorCode":null,"errorMessage":"Multiple errors on cleanup stage","messagePattern":"Multiple errors on cleanup stage","errorType":"exception","errorClass":"CleanupError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_app.py","lineNumber":447,"sourceCode":"\n            if not isinstance(ctx, AbstractAsyncContextManager):\n                ctx = asynccontextmanager(cb)(app)  # type: ignore[arg-type]\n\n            await ctx.__aenter__()\n            self._exits.append(ctx)\n\n    async def _on_cleanup(self, app: Application) -> None:\n        errors = []\n        for it in reversed(self._exits):\n            try:\n                await it.__aexit__(None, None, None)\n            except (Exception, asyncio.CancelledError) as exc:\n                errors.append(exc)\n        if errors:\n            if len(errors) == 1:\n                raise errors[0]\n            else:\n                raise CleanupError(\"Multiple errors on cleanup stage\", errors)\n","sourceCodeStart":429,"sourceCodeEnd":448,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_app.py#L429-L448","documentation":"Raised as aiohttp.CleanupError (a RuntimeError subclass) when Application shutdown runs the registered cleanup contexts and two or more of them raise during their __aexit__. aiohttp collects every cleanup exception (instead of aborting on the first) so you see all failures, then wraps them into one CleanupError whose .exceptions property exposes the original list. Only when a single cleanup fails does aiohttp re-raise that exception as-is; the combined form is reserved for the multi-failure case.","triggerScenarios":"Multiple async cleanup context managers (registered via app.cleanup_ctx.add(...) or via the cleanup_ctx list) whose __aexit__ methods raise during Application.cleanup(). Each registered CleanupContext that throws contributes one entry; if the count is > 1, CleanupError is raised from CleanupContext._on_cleanup at aiohttp/web_app.py:447.","commonSituations":"During app shutdown / test teardown with two independent background tasks (e.g. a database engine pool and a redis pool) that both fail to close, or when a cleanup handler raises because the resource it tracks was never successfully started. Also appears in test suites that shut down the app while the event loop is closing, surfacing latent bugs masked during normal startup.","solutions":["Read the CleanupError.exceptions list (err.exceptions) in your top-level except to see each underlying cause, instead of only str(err) which just says 'Multiple errors on cleanup stage'.","Make each cleanup context's __aexit__ defensive: log and swallow non-fatal errors, or wrap risky teardown in try/except so one failure cannot cascade into others.","Order cleanup contexts so dependent resources shut down before their owners (cleanup_ctx runs teardown in reverse registration order).","Reproduce shutdown in isolation with app.cleanup() in a test to identify which contexts actually throw."],"exampleFix":"// before\nasync def main():\n    app = web.Application()\n    app.cleanup_ctx.append(db_ctx)\n    app.cleanup_ctx.append(cache_ctx)\n    runner = web.AppRunner(app)\n    await runner.setup()\n    ...\n    await runner.cleanup()  # raises CleanupError, originals lost\n\n# after\ntry:\n    await runner.cleanup()\nexcept CleanupError as err:\n    for exc in err.exceptions:\n        logger.error(\"cleanup failed: %r\", exc)\n    raise","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    await runner.cleanup()\nexcept CleanupError as err:\n    for exc in err.exceptions:\n        logger.error(\"cleanup failure: %r\", exc)\n    # decide whether to re-raise the first underlying exception\n    raise err.exceptions[0] if len(err.exceptions) == 1 else err","preventionTips":["Make every cleanup context's __aexit__ defensive so a single failure cannot cascade.","Log the .exceptions list rather than str(err) which only shows the static message.","Register cleanup_ctx in dependency order; teardown runs reverse.","Test app.cleanup() in isolation to surface latent teardown bugs."],"tags":["lifecycle","shutdown","cleanup","async"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}