Skip to content

AsyncExitStack with gather/TaskGroup doesn't propagate exceptions as expected #137517

@PickyPony

Description

@PickyPony

Summary

When using AsyncExitStack with gather() or TaskGroup() to enter async context managers that contain background tasks (e.g., created with TaskGroup.create_task()), exceptions from these background tasks are not immediately propagated. Instead, the main execution continues, and exceptions are only shown when the program terminates or the context manager is explicitly exited.

Expected Behavior

await gather(stack.enter_async_context(context())) and await stack.enter_async_context(context()) should result in the same behavior.

Actual Behavior

When using await asyncio.gather(stack.enter_async_context(context())), exceptions from background tasks are suppressed/delayed, and the main execution continues with "context running" being printed repeatedly.

Minimal Reproduction Case

import asyncio
from asyncio import TaskGroup
from contextlib import asynccontextmanager, AsyncExitStack


async def run():
    await asyncio.sleep(0.5)
    raise ValueError("Error")


@asynccontextmanager
async def context():
    async with TaskGroup() as tg:
        tg.create_task(run())  # Background task that will fail
        yield "something"


async def main():
    async with AsyncExitStack() as stack:
        # BUG: Error gets not directly propagated
        await asyncio.gather(stack.enter_async_context(context()))

        # WORKS: The error gets directly propagated (when uncommented)
        # await stack.enter_async_context(context())
        
        while True:
            print("context running")  # This continues to print despite the error
            await asyncio.sleep(1)


asyncio.run(main(), debug=True)

Bug Case Output

context running
Task <Task finished name='Task-3' coro=<run() done, defined at /home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py:6> exception=ValueError('Error')> has errored out but its parent task <Task finished name='Task-2' coro=<AsyncExitStack.enter_async_context() done, defined at /usr/lib/python3.14/contextlib.py:654> result='something'> is already completed
task: <Task finished name='Task-3' coro=<run() done, defined at /home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py:6> exception=ValueError('Error')>
Traceback (most recent call last):
  File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 8, in run
    raise ValueError("Error")
ValueError: Error
context running
context running
context running

!!!KeyboardInterrupt!!!

  + Exception Group Traceback (most recent call last):
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 31, in <module>
  |     asyncio.run(main())
  |     ~~~~~~~~~~~^^^^^^^^
  |   File "/usr/lib/python3.14/asyncio/runners.py", line 204, in run
  |     return runner.run(main)
  |            ~~~~~~~~~~^^^^^^
  |   File "/usr/lib/python3.14/asyncio/runners.py", line 127, in run
  |     return self._loop.run_until_complete(task)
  |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  |   File "/usr/lib/python3.14/asyncio/base_events.py", line 719, in run_until_complete
  |     return future.result()
  |            ~~~~~~~~~~~~~^^
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 19, in main
  |     async with AsyncExitStack() as stack:
  |                ~~~~~~~~~~~~~~^^
  |   File "/usr/lib/python3.14/contextlib.py", line 768, in __aexit__
  |     raise exc
  |   File "/usr/lib/python3.14/contextlib.py", line 751, in __aexit__
  |     cb_suppress = await cb(*exc_details)
  |                   ^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib/python3.14/contextlib.py", line 235, in __aexit__
  |     await self.gen.athrow(value)
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 13, in context
  |     async with TaskGroup() as tg:
  |                ~~~~~~~~~^^
  |   File "/usr/lib/python3.14/asyncio/taskgroups.py", line 72, in __aexit__
  |     return await self._aexit(et, exc)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib/python3.14/asyncio/taskgroups.py", line 174, in _aexit
  |     raise BaseExceptionGroup(
  |     ...<2 lines>...
  |     ) from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 8, in run
    |     raise ValueError("Error")
    | ValueError: Error
    +------------------------------------
Process finished with exit code 1

Normal Case Output

context running
  + Exception Group Traceback (most recent call last):
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 31, in <module>
  |     asyncio.run(main())
  |     ~~~~~~~~~~~^^^^^^^^
  |   File "/usr/lib/python3.14/asyncio/runners.py", line 204, in run
  |     return runner.run(main)
  |            ~~~~~~~~~~^^^^^^
  |   File "/usr/lib/python3.14/asyncio/runners.py", line 127, in run
  |     return self._loop.run_until_complete(task)
  |            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  |   File "/usr/lib/python3.14/asyncio/base_events.py", line 719, in run_until_complete
  |     return future.result()
  |            ~~~~~~~~~~~~~^^
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 19, in main
  |     async with AsyncExitStack() as stack:
  |                ~~~~~~~~~~~~~~^^
  |   File "/usr/lib/python3.14/contextlib.py", line 768, in __aexit__
  |     raise exc
  |   File "/usr/lib/python3.14/contextlib.py", line 751, in __aexit__
  |     cb_suppress = await cb(*exc_details)
  |                   ^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib/python3.14/contextlib.py", line 235, in __aexit__
  |     await self.gen.athrow(value)
  |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 13, in context
  |     async with TaskGroup() as tg:
  |                ~~~~~~~~~^^
  |   File "/usr/lib/python3.14/asyncio/taskgroups.py", line 72, in __aexit__
  |     return await self._aexit(et, exc)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib/python3.14/asyncio/taskgroups.py", line 174, in _aexit
  |     raise BaseExceptionGroup(
  |     ...<2 lines>...
  |     ) from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/home/pickypony/PycharmProjects/PythonProject/asynctest/bug.py", line 8, in run
    |     raise ValueError("Error")
    | ValueError: Error
    +------------------------------------

Process finished with exit code 1

Use Case

My goal is to enter multiple async context managers concurrently for performance reasons (parallel initialization), while maintaining proper exception propagation from any background tasks running within these context managers.

CPython versions tested on:

3.13, 3.14

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibPython modules in the Lib dirtopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions