From fbab57e9c75751cd489bf36eb20078711bf6c6e1 Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Sat, 17 Oct 2026 21:27:03 +0200 Subject: [PATCH] Fix TaskGroup.start() losing child exception during cancellation on asyncio --- changelog.d/1356.fixed.rst | 3 +++ src/anyio/_backends/_asyncio.py | 9 ++++++++- tests/test_taskgroups.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1356.fixed.rst diff --git a/changelog.d/1356.fixed.rst b/changelog.d/1356.fixed.rst new file mode 100644 index 0000000..dd37230 --- /dev/null +++ b/changelog.d/1356.fixed.rst @@ -0,0 +1,3 @@ +Fixed ``TaskGroup.start()`` on the asyncio backend discarding a non-cancellation +exception raised by the child task during cleanup when ``start()`` was cancelled +before ``task_status.started()`` was called diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 874374a..3e6fcac 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -1040,6 +1040,7 @@ class TaskGroup(abc.TaskGroup): # point between, the task group is cancelled and this method never proceeds to # process the completed future. That's why we have to have a shielded cancel # scope here. + exc_to_raise: BaseException | None = None try: await future except BaseException: @@ -1049,7 +1050,13 @@ class TaskGroup(abc.TaskGroup): with CancelScope(shield=True): await handle.wait() - raise + if handle.status is TaskHandle.Status.FAILED: + exc_to_raise = handle._exception + else: + raise + + if exc_to_raise is not None: + raise exc_to_raise if return_handle: handle._start_value = future.result() diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 109d279..5241999 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -308,6 +308,22 @@ async def test_start_cancelled() -> None: assert not finished +async def test_start_cancelled_child_raises() -> None: + """Regression test for #1356.""" + + async def taskfunc(*, task_status: TaskStatus) -> None: + try: + await sleep_forever() + finally: + raise RuntimeError("cleanup failed") + + async with create_task_group() as tg: + with CancelScope() as scope: + scope.cancel() + with pytest.raises(RuntimeError, match="cleanup failed"): + await tg.start(taskfunc) + + @pytest.mark.parametrize("anyio_backend", asyncio_params) async def test_start_native_host_cancelled() -> None: started = finished = False -- 2.55.0.windows.3