6
votes

Why is the CancelledError not caught in this example?

import asyncio

q = asyncio.Queue()

async def getter():
    try:
        v = await q.get()
        print(f"getter got {v}")
    except asyncio.CancelledError:
        print("getter cancelled")

async def test():
    task = asyncio.ensure_future(getter())
    task.cancel()
    await task

def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(test())

if __name__ == '__main__':
    main()

I excpected to get the "getter cancelled" message, but received a stack trace instead:

Traceback (most recent call last):
  File "ce.py", line 22, in 
    main()
  File "ce.py", line 19, in main
    loop.run_until_complete(test())
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 468, in run_until_complete
    return future.result()
concurrent.futures._base.CancelledError

Task.cancel states:

This arranges for a CancelledError to be thrown into the wrapped coroutine on the next cycle through the event loop. The coroutine then has a chance to clean up or even deny the request using try/except/finally.

1

1 Answers

8
votes

The problem is that getter didn't even start executing, which you can confirm by adding a print at its beginning. Since the try block was never entered, the except didn't run either.

This happens because, in contrast to await, ensure_future doesn't start executing the coroutine right away, it just schedules it to run at the next event loop iteration, like call_soon does for ordinary functions. Since you cancel the task immediately, it gets removed from the runnable set and its coroutine gets closed without ever having started.

Add an await asyncio.sleep(0) before task.cancel() and you should observe the behavior you expect. I suspect you don't need to make such change in your actual code - in the unlikely case that the task gets cancelled before it ran, as in the example, it won't get a chance to acquire the resources that try/except cleans up in the first place.

Two tangential remarks:

  • You probably want to re-raise asyncio.CancelledError after handling it, otherwise it will be suppressed. That's not a problem in getter as shown in the question, but it could be an issue if the code were buried in a function call. Even better, consider using finally or with, which propagate the exception and ensure that the resources are released regardless of exception type.

  • When you need to create a task and run a coroutine, loop.create_task is preferred to asyncio.ensure_future. In short, although both do the same thing for coroutines, create_task makes the intention clearer; ensure_future is designed to accept a wider range of objects and procure a future of an unspecified type.