Skip to content

gh-135736: Fix asyncio.TaskGroup swallowing errors on GeneratorExit - #154538

Open
sreehariannam wants to merge 8 commits into
python:mainfrom
sreehariannam:gh-135736-taskgroup-generatorexit
Open

gh-135736: Fix asyncio.TaskGroup swallowing errors on GeneratorExit#154538
sreehariannam wants to merge 8 commits into
python:mainfrom
sreehariannam:gh-135736-taskgroup-generatorexit

Conversation

@sreehariannam

Copy link
Copy Markdown

Fixes gh-135736.

Problem

When the body of an async with asyncio.TaskGroup() block exits via a bare GeneratorExit (for example, because it's inside an async generator that gets closed with aclose()), the GeneratorExit was wrapped in a BaseExceptionGroup like any other exception. Since callers of aclose()/athrow() only know how to handle GeneratorExit (or StopAsyncIteration) coming back out, this surfaced as a spurious, hard-to-debug ExceptionGroup: unhandled errors in a TaskGroup instead of a clean generator close.

Fix

TaskGroup._aexit() now treats a GeneratorExit raised by the async with body itself as a "base error" (like SystemExit/KeyboardInterrupt) and re-raises it directly instead of wrapping it, so aclose() sees the exception type it expects.

This is deliberately scoped to the body-exception path only. TaskGroup._is_base_error() itself (used separately in _on_task_done() for exceptions raised by child tasks) is untouched — a task raising GeneratorExit internally is not a "close this generator" signal and continues to be wrapped in the group as before (covered by a new test, test_taskgroup_20e).

Since only one exception can propagate out through GeneratorExit-closing APIs, any sibling task errors that would otherwise be silently discarded in this path are now reported via loop.call_exception_handler() instead of disappearing (test_taskgroup_20c).

Testing

Added four tests (test_taskgroup_20b-test_taskgroup_20e) covering: bare propagation, propagation alongside a failing sibling task (checking the exception handler is invoked), a faithful reproduction of the original report via a real async generator + aclose(), and the unchanged task-level behavior. Verified each new test fails without the fix by reverting it locally and re-running. Full test_taskgroups suite (126 tests) passes.

Comment thread Lib/asyncio/taskgroups.py Outdated
assert not self._tasks

if self._base_error is not None:
if isinstance(self._base_error, GeneratorExit):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the exception handler should be called for all base errors.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed — the reporting is no longer gated on isinstance(self._base_error, GeneratorExit), so suppressed sibling-task errors are now reported via call_exception_handler whenever any base error (SystemExit, KeyboardInterrupt, etc.) causes the TaskGroup to exit, not just GeneratorExit. Added test_taskgroup_20f covering the non-GeneratorExit case.

@python-cla-bot

python-cla-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

…, not just GeneratorExit

Per review feedback from kumaraditya303: the exception-handler reporting
of suppressed sibling-task errors was previously gated on
isinstance(self._base_error, GeneratorExit), but any base error
(SystemExit, KeyboardInterrupt, etc.) causes the same silent discarding
of collected task errors. Broaden the reporting to run whenever
self._base_error is set, and add a regression test covering a
non-GeneratorExit base error.
@sreehariannam
sreehariannam force-pushed the gh-135736-taskgroup-generatorexit branch from 331b20c to f878aeb Compare July 24, 2026 15:44
… a custom BaseException

TaskGroup._is_base_error() only recognizes SystemExit and KeyboardInterrupt
(GeneratorExit is handled separately); an arbitrary BaseException subclass
like MyBaseExc never sets self._base_error, so it takes the
BaseExceptionGroup-wrapping path instead of the direct-raise path this test
meant to exercise, causing CI failures across all platforms. Use
KeyboardInterrupt (matching the existing test_taskgroup_20) instead.
@sreehariannam

Copy link
Copy Markdown
Author

@kumaraditya303 Following up — I've addressed your comment about the exception handler being gated on GeneratorExit: the reporting block in _aexit now fires for any base error (self._base_error is not None), not just GeneratorExit. Added test_taskgroup_20f covering the non-GeneratorExit case (KeyboardInterrupt with a suppressed sibling exception).

CI is green. Would appreciate a re-review when you have a chance.

@kumaraditya303

Copy link
Copy Markdown
Contributor

An alternate fix was merged, if you can restrict this PR to just the logging part then I can review and merge it.

Per review feedback from kumaraditya303: an alternate fix for the
GeneratorExit-wrapped-in-BaseExceptionGroup bug was merged separately
(pythongh-154649), so drop the now-redundant GeneratorExit handling from
_is_base_error()/_base_error here. Keep only the independent part:
reporting sibling task errors via loop.call_exception_handler() when
they would otherwise be silently discarded because the "async with"
body exited with a SystemExit or KeyboardInterrupt.
@sreehariannam

Copy link
Copy Markdown
Author

Done — dropped the _is_base_error/GeneratorExit handling (now redundant with #154649) and the tests that overlapped/conflicted with it. The PR is now scoped to just the remaining independent issue: sibling task errors were being silently discarded whenever the async with body exited via SystemExit/KeyboardInterrupt (the self._base_error short-circuit in _aexit() never reported self._errors before raising). They're now reported via loop.call_exception_handler(), covered by test_taskgroup_20b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GeneratorExit and asyncio.TaskGroup - missing from _is_base_error?

2 participants