Skip to content

SystemExit in asyncio task raises noisy warnings #159401

Open

Description

@marcan

Bug report

This might be related to #102572 and/or #108549, but I think it's a distinct issue.

import asyncio
import sys

async def task():
    try:
        sys.exit(2)
    except BaseException as e:
        print(f"task() saw exception: {e!r}")
        # print(traceback.format_exc())
        raise

async def main():
    try:
        t = asyncio.create_task(task())
        await asyncio.sleep(2)
        await t
    except BaseException as e:
        print(f"main() saw exception: {e!r}")
        # print(traceback.format_exc())
        raise

try:
    asyncio.run(main())
except BaseException as e:
    print(f"top level saw exception: {e!r}")
    # print(traceback.format_exc())
    raise

This prints:

task() saw exception: SystemExit(2)
main() saw exception: CancelledError()
top level saw exception: SystemExit(2)
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<task() done, defined at /home/marcan/tmp/asynctest.py:4> exception=SystemExit(2)>
[ snip traceback ]

It seems when the subtask raises SystemExit, the scheduler cancels all other tasks (including the main task), then re-raises the SystemExit to the top level. However, since main() was cancelled, the subtask is never awaited. This then prints an arguably spurious warning.

In this case, the exception was effectively retrieved: it cancelled main and even bubbled up outside the scheduler. I think the special-case handling of SystemExit here should also silence the task exception retrieval warning.

If you remove the sleep, the warning does not occur, despite the fact that main() still gets the CancelledError (it is not seeing the SystemExit exception itself). So in this case, the "intent" by main() to retrieve the exception is sufficient to silence the warning, despite the fact that the exception wasn't actually propagated there.

Uncommenting the print(traceback.format_exc()) lines shows asyncio internals "leaking" into the tracebacks, but only in the case with sleep.

With sleep
task() saw exception: SystemExit(2)
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2

main() saw exception: CancelledError()
Traceback (most recent call last):
  File "/usr/lib64/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 707, in run_until_complete
    self.run_forever()
    ~~~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 678, in run_forever
    self._run_once()
    ~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 2057, in _run_once
    handle._run()
    ~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/events.py", line 94, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 16, in main
    await asyncio.sleep(2)
  File "/usr/lib64/python3.14/asyncio/tasks.py", line 705, in sleep
    return await future
           ^^^^^^^^^^^^
asyncio.exceptions.CancelledError

top level saw exception: SystemExit(2)
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 24, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/usr/lib64/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 707, in run_until_complete
    self.run_forever()
    ~~~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 678, in run_forever
    self._run_once()
    ~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 2057, in _run_once
    handle._run()
    ~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/events.py", line 94, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2

Task exception was never retrieved
future: <Task finished name='Task-2' coro=<task() done, defined at /home/marcan/tmp/asynctest.py:5> exception=SystemExit(2)>
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 24, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/usr/lib64/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 707, in run_until_complete
    self.run_forever()
    ~~~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 678, in run_forever
    self._run_once()
    ~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/base_events.py", line 2057, in _run_once
    handle._run()
    ~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/events.py", line 94, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2
Without sleep
task() saw exception: SystemExit(2)
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2

main() saw exception: CancelledError()
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 17, in main
    await t
asyncio.exceptions.CancelledError

top level saw exception: SystemExit(2)
Traceback (most recent call last):
  File "/home/marcan/tmp/asynctest.py", line 24, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/home/marcan/tmp/asynctest.py", line 7, in task
    sys.exit(2)
    ~~~~~~~~^^^
SystemExit: 2

CPython versions tested on:

3.14

Operating systems tested on:

No response

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Oct 14, 2026
  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Oct 14, 2026
  3. YvesDup commented on Oct 14, 2026

    @YvesDup
    Contributor

    This then prints an arguably spurious warning. ...... I think the special-case handling of SystemExit here should also silence the task exception retrieval warning.

    The Task exception was never retrieved message and the re-raise exception, occurs when the loop was closing and all the tasks/futures are destroyed (see Task.__del__ and Future.__del__).
    To prevent this, there are 2 options:

    • consume the task exception as for instance print(t.exception()) in the except block of main task.
    • install set_exception_handleron the current loop to intercept all error messages.

    IMO that seems a normal behavior

    ...., despite the fact that main() still gets the CancelledError (it is not seeing the SystemExit exception itself)

    That's look like #108549, fixed in the main branch. Could you make a test please ?

  4. marcan commented on Oct 14, 2026

    @marcan
    Author

    So I traced things and in both cases, the SystemExit bubbles out of the original task and stops the event loop. Everything after that point happens via Runner.__exit__() -> Runner.close() which cancels all tasks.

    The fix for #108549 changes the behavior without the sleep (main() then observes the SystemExit), but not with the sleep.

    IMO thiat seems a normal behavior

    AIUI the point of that message is to indicate that an exception was not handled, and lost. In this case, the original exception bubbled all the way out of the event loop, caused main() to be canceled, which in turn caused it not to await on the original task. Since it is being propagated up to the top level, it isn't actually being lost.

    Obviously you can work around it by wrapping the entirety of main in a cleanup block that guarantees that t always has its exception inspected, but that seems like a workaround for this behavior more than anything.

    One fix that might make sense:

    --- asyncio/tasks.py
    +++ asyncio/tasks.py
    @@ -303,6 +303,7 @@
                 super().cancel()  # I.e., Future.cancel(self).
             except (KeyboardInterrupt, SystemExit) as exc:
                 super().set_exception(exc)
    +            self._log_traceback = False
                 raise
             except BaseException as exc:
                 super().set_exception(exc)

    (and the equivalent change in _asyncio)

    Since those two exceptions are being re-raised (and therefore propagated, not just stored in the Future object), there is no need to log them if the task is not awaited, as the caller can be presumed to have handled the exception appropriately.

    I'm not sure what is going on with the different traceback though. I suspect that's another bug, or at least another awkward issue. The traceback bubbled up to the top level probably shouldn't be modified/replaced by the cleanup when all tasks are canceled. Or, at least, when main() observes the traceback, it should also have its own stack frames added to the traceback (I haven't checked if the #108549 change does this or changes that behavior, I only looked at the pure-python side).

  5. YvesDup commented on Oct 14, 2026

    @YvesDup
    Contributor

    I am 0 about your proposal, because this isn't really a bug, even though SystemExit is an exception with specific behaviors.

    I'm not sure what is going on with the different traceback though. I suspect that's another bug, or at least another awkward issue ....

    Does your comment in this paragraph refer when we remove the await asyncio.sleep(2) from your main task?

    @gaoanze888, you're really in a hurry. It might be a good idea to let the OP decide how to proceed with this issue.

  6. marcan commented on Oct 14, 2026

    @marcan
    Author

    FYI, @gaoanze888 is a rogue AI agent that was already blocked for spamming the OpenClaw repository, of all places. Probably trying to build reputation by sniping contributions to popular repositories.

  7. marcan commented on Oct 14, 2026

    @marcan
    Author

    Does your comment in this paragraph refer when we remove the await asyncio.sleep(2) from your main task?

    Yes. Without the sleep, the traceback lacks all useful context about how that propagated up. I would expect it to show the await in main() since it reached that point and main() observed the exit. With the sleep, the link to main() is not visible but at least you can see it killed the whole loop (which explains why it is reaching the top level) so that at least makes some sense.

    I am 0 about your proposal, because this isn't really a bug, even though SystemExit is an exception with specific behaviors.

    SystemExit has those specific behaviors because it's supposed to immediately and cleanly exit the program without error prints (absent other misbehavior). In this case the exit works, there is nothing wrong with the program, yet a warning is printed. I would call that a bug, personally. There's no good reason to print a SystemExit traceback when the application is, in fact, correctly exiting.

  8. YvesDup commented on Oct 14, 2026

    @YvesDup
    Contributor

    traceback is not appropriate with asynchrone code as asyncio. Peharps the pstree tool (new in 3.14) can be more useful (see https://docs.python.org/3.14/library/asyncio-tools.html).

    I would call that a bug, personally. There's no good reason to print a SystemExit traceback when the application is, in fact, correctly exiting.

    You are right about printing traceback (see https://docs.python.org/3.15/builtins/exceptions.html#SystemExit), this shouldn't be printed. That said, this proposal seems to me to be more an enhanced than a bug. Moreover the insertion of the self._log_traceback = False statement as you require should concern SystemExit only, not KeybordInterrupt exception.

    A core dev opinion will be welcome.

  9. marcan commented on Oct 16, 2026

    @marcan
    Author

    10. Moreover the insertion of the self._log_traceback = False statement as you require should concern SystemExit only, not KeybordInterrupt exception.

    No, you need it for both. For KeyboardInterrupt what happens is you get the actual exception traceback and then you get the asyncio warning with the traceback again.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions