asyncio.gather will always keep waiting for every task it manages.
If one task raises an exception the others do not stop by themselves.
This becomes visible with IO bound work since those tasks remain in a
pending state forever if they never reach an await that reacts to
cancellation.
A reliable pattern is to catch the first failure and cancel the
remaining tasks explicitly. After cancellation we await them again
so the event loop can process the cancellation properly.
This prevents any hang and the original exception still propagates.
# Stop other tasks at first failure
import asyncio
async def taskA():
try:
while True:
print("A running")
await asyncio.sleep(1)
except asyncio.CancelledError:
print("A cancelled")
raise
async def taskB():
await asyncio.sleep(2)
raise RuntimeError("Failure in B")
async def main():
one = asyncio.create_task(taskA())
two = asyncio.create_task(taskB())
try:
await asyncio.gather(one, two)
except Exception as err:
one.cancel()
await one
raise err
asyncio.run(main())
With this approach the moment taskB fails the entire group stops and the
program exits without getting stuck.
If you want you can even extend this pattern with network timeouts to make the
system more reliable
gather. And please take a look at documentation -gather()doesn't cancel tasks when it encounters an exceptionasyncio.gatherdoes, why did you expect that? You probably want to useasyncio.TaskGroupinstead.