-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
Description
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
Labels
Projects
Status