Search code examples
pythonasynchronousasync-awaitpython-asyncio

Coroutines are stopped in asyncio.gather(*aws, return_exceptions=False) when exception happens


My question is from this post. I'll describe it here.

I have the following Python code:

import asyncio, time

async def fn1(x):
    await asyncio.sleep(3) 
    print(f"fn1 is called with x={x}")
    return "fn1 SUCCESS"

async def fn2(y):
    await asyncio.sleep(2) 
    print(f"fn2 is called with y={y}")
    raise asyncio.TimeoutError() 
    print(y, '*'*10)
    return "fn2 SUCCESS"

async def main():
    print("start:",time.ctime())
    result = ['No results']
    try:
        result = await asyncio.gather(
            fn1("fn1"),
            fn2("fn2"),
            return_exceptions=False, 
            # return_exceptions=True, 
            )
    except Exception as e:
        print('e:', type(e), str(e))
    print("end:",time.ctime())
    print(result)

asyncio.run(main())

This is the result I got:

start: Mon Sep 30 17:25:28 2024
fn2 is called with y=fn2
e: <class 'TimeoutError'> 
end: Mon Sep 30 17:25:30 2024
['No results']

According to awaitable asyncio.gather(*aws, return_exceptions=False)¶,

If return_exceptions is False (default), the first raised exception is immediately propagated to the task that awaits on gather(). Other awaitables in the aws sequence won’t be cancelled and will continue to run.

But this is contrary to the result I got. Why fn1("fn1") coroutine does not get to finish running? If it finishes running, the line print(f"fn1 is called with x={x}") should print it out.

Then, I made a simple change to fn1, where the "sleep" time is shorter:

async def fn1(x):
    await asyncio.sleep(3-1) 
    print(f"fn1 is called with x={x}")
    return "fn1 SUCCESS"

In this time the coroutine fn1 gets to finish:

start: Mon Sep 30 17:30:29 2024
fn1 is called with x=fn1
fn2 is called with y=fn2
e: <class 'TimeoutError'> 
end: Mon Sep 30 17:30:31 2024
['No results']

This is unexpected for me! The behaviours here seem to be not in line with the doc that "Other awaitables in the aws sequence won’t be cancelled and will continue to run." Can you please explain why?


Solution

  • The task continues to run, but your program finishes immediately by returning the co-routine passed to asyncio.run. It can only actually be executed if the control returns to the event loop.

    Simply add an await asyncio.sleep(2) in main, before print("end") and you should see the print from fn1. Of course its return values won't be included in results, as .gather already returned.

    If you want to get its return value, you have to wrap the call to fn1 in a task manually, instead of passing the co-routine directly to .gather (in which case it creates the wrapping task, but there is no direct way to get a reference to it):

    ...
    
    async def main():
        print("start:",time.ctime())
        result = ['No results']
        try:
            result = await asyncio.gather(
                fn1_task:=asyncio.create_task(fn1("fn1")),
                fn2("fn2"),
                return_exceptions=False, 
                # return_exceptions=True, 
                )
        except Exception as e:
            print('e:', type(e), str(e))
        print(await fn1_task)
        print("end:",time.ctime())
        print(result)
    
    asyncio.run(main())