A very important distinction, which is easy to miss, is the default behavior of these two functions, when it comes to exceptions.
I'll use this example to simulate a coroutine that will raise exceptions, sometimes -
import asyncio import random async def a_flaky_tsk(i): await asyncio.sleep(i) # bit of fuzz to simulate a real-world example if i % 2 == 0: print(i, "ok") else: print(i, "crashed!") raise ValueError coros = [a_flaky_tsk(i) for i in range(10)]
await asyncio.gather(*coros) outputs -
0 ok 1 crashed! Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 20, in <module> asyncio.run(main()) File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/runners.py", line 43, in run return loop.run_until_complete(main) File "/Users/dev/.pyenv/versions/3.8.2/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete return future.result() File "/Users/dev/PycharmProjects/trading/xxx.py", line 17, in main await asyncio.gather(*coros) File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError
As you can see, the coros after index 1 never got to execute. Future returned by gather() is done at that point (unlike wait()) and program terminates, but if you could keep the program alive, other coroutines still would have chance to run:
async def main(): coros = [a_flaky_tsk(i) for i in range(10)] await asyncio.gather(*coros) if __name__ == '__main__': loop = asyncio.new_event_loop() loop.create_task(main()) loop.run_forever() # 0 ok # 1 crashed! # Task exception was never retrieved # .... # 2 ok # 3 crashed! # 4 ok # 5 crashed! # 6 ok # 7 crashed! # 8 ok # 9 crashed!
But await asyncio.wait(coros) continues to execute tasks, even if some of them fail (Future returned by wait() is not done, unlike gather()) -
0 ok 1 crashed! 2 ok 3 crashed! 4 ok 5 crashed! 6 ok 7 crashed! 8 ok 9 crashed! Task exception was never retrieved future: <Task finished name='Task-10' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()> Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError Task exception was never retrieved future: <Task finished name='Task-8' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()> Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError Task exception was never retrieved future: <Task finished name='Task-2' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()> Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError Task exception was never retrieved future: <Task finished name='Task-9' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()> Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError Task exception was never retrieved future: <Task finished name='Task-3' coro=<a_flaky_tsk() done, defined at /Users/dev/PycharmProjects/trading/xxx.py:6> exception=ValueError()> Traceback (most recent call last): File "/Users/dev/PycharmProjects/trading/xxx.py", line 12, in a_flaky_tsk raise ValueError ValueError
Of course, this behavior can be changed for both by using -
asyncio.gather(..., return_exceptions=True)
or,
asyncio.wait([...], return_when=asyncio.FIRST_EXCEPTION)
But it doesn't end here!
Notice: Task exception was never retrieved in the logs above.
asyncio.wait() won't re-raise exceptions from the child tasks until you await them individually. (The stacktrace in the logs are just messages, they cannot be caught!)
done, pending = await asyncio.wait(coros) for tsk in done: try: await tsk except Exception as e: print("I caught:", repr(e))
Output -
0 ok 1 crashed! 2 ok 3 crashed! 4 ok 5 crashed! 6 ok 7 crashed! 8 ok 9 crashed! I caught: ValueError() I caught: ValueError() I caught: ValueError() I caught: ValueError() I caught: ValueError()
On the other hand, to catch exceptions with asyncio.gather(), you must -
results = await asyncio.gather(*coros, return_exceptions=True) for result_or_exc in results: if isinstance(result_or_exc, Exception): print("I caught:", repr(result_or_exc))
(Same output as before)