Repository navigation
SystemExit in asyncio task raises noisy warnings #159401
Description
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())
raiseThis 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
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Activity
YvesDup commentedon Oct 14, 2026
The
Task exception was never retrievedmessage and the re-raise exception, occurs when the loop was closing and all the tasks/futures are destroyed (seeTask.__del__andFuture.__del__).To prevent this, there are 2 options:
print(t.exception())in theexceptblock ofmaintask.set_exception_handleron the current loop to intercept all error messages.IMO that seems a normal behavior
That's look like #108549, fixed in the
mainbranch. Could you make a test please ?marcan commentedon Oct 14, 2026
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 theSystemExit), but not with the sleep.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
talways has its exception inspected, but that seems like a workaround for this behavior more than anything.One fix that might make sense:
(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).YvesDup commentedon Oct 14, 2026
I am 0 about your proposal, because this isn't really a bug, even though SystemExit is an exception with specific behaviors.
Does your comment in this paragraph refer when we remove the
await asyncio.sleep(2)from yourmaintask?@gaoanze888, you're really in a hurry. It might be a good idea to let the OP decide how to proceed with this issue.
marcan commentedon Oct 14, 2026
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.
marcan commentedon Oct 14, 2026
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 andmain()observed the exit. With the sleep, the link tomain()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.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.
YvesDup commentedon Oct 14, 2026
tracebackis not appropriate with asynchrone code asasyncio. Peharps the pstree tool (new in 3.14) can be more useful (see https://docs.python.org/3.14/library/asyncio-tools.html).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 = Falsestatement as you require should concernSystemExitonly, notKeybordInterruptexception.A core dev opinion will be welcome.
marcan commentedon Oct 16, 2026
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.