4 Ways to Suppress Asyncio CancelledError
You can suppress CancelledError exceptions in asyncio by handling them within the task or in the caller that canceled the task.
Alternatively, they can be suppressed in one line by awaiting asyncio.gather() and configuring it to return exceptions, or via the contextlib.suppress() context manager.
In this tutorial, you will discover how to suppress the CancelledError in asyncio programs.
Let's get started.
What is Asyncio Task Cancellation?
Asyncio tasks can be canceled.
This can be achieved by calling the cancel() method on the asyncio.Task. This will request that the task be canceled as soon as possible and returns True if the request was successful or False if it was not, e.g. the task is already done.
Request the Task to be cancelled. This arranges for a CancelledError exception to be thrown into the wrapped coroutine on the next cycle of the event loop.
-- Coroutines and Tasks
For example:
...
# cancel a task
if task.cancel():
# request successful
The next time that the target task that was canceled resumes, a CancelledError exception will be raised.
This exception will bubble up to the top level of the task and cause it to stop running.
You can learn more about the mechanics of cancellation a task in the tutorial:
You can also learn more about task cancellation best practices in the tutorial:
Need to Suppress the CancelledError When Canceling Tasks
When an asyncio task is canceled it will raise a CancelledError the next time the task resumes.
The raised CancelledError can be a problem, for example:
- The CancelledError may propagate up and terminate the target task.
- The CancelledError may propagate up and terminate the calling task.
- The CancelledError may continue to propagate and terminate the program.
You can learn more about how the CancelledError propagates in the tutorial:
If we know that a target task is canceled and will raise a CancelledError exception, we may not be so interested in handling the CancelledError exception.
In turn, we may want to explicitly suppress the expected CancelledError exception in a canceled task.
Given that we may cancel tasks many times in our program, the repeated boilerplate for handling task cancellation may become undesirable.
How can we suppress the CancelledError when canceling a task?
How to Suppress CancelledError
There are perhaps four ways to handle or suppress the CancelledError when canceling a task, they are:
- Suppress within task
- Suppress within caller
- Suppress using asyncio.gather()
- Suppress using contextlib.suppress()
Let's take a closer look at each in turn.
Suppress CancelledError within Task
A CancelledError exception will be raised within a task when it is canceled.
The task itself can suppress the raised CancelledError exception.
This can be achieved with a try-except block around the body of the task that handles the CancelledError and then does not re-raise it.
For example:
...
try:
# body of the task
...
except asyncio.CancelledError:
pass
This will ensure that the task will terminate, e.g. stop executing the task body, but does not raise a CancelledError exception.
Note that this approach will not work if the task is canceled while it is pending. In this case, the CancelledError exception will still be raised.
Suppress CancelledError within Caller
A CancelledError exception can be suppressed by the caller of the task.
That is the task above the canceled task that is controlling it or that has requested that it be canceled.
This can be achieved by adding a try-except around the awaiting the task.
For example:
...
try:
# await the cancelled task
await task
except asyncio.CancelledError:
pass
This is a common pattern when canceling tasks in asyncio programs, called "cancel and wait".
You can learn more about the cancel and wait pattern in the tutorial:
This will ensure that the CancelledError exception is suppressed at the appropriate scope and may be the most common way to handle a CancelledError exception.
Suppress CancelledError with asyncio.gather()
A CancelledError exception can be suppressed by using asyncio.gather().
The asyncio.gather() function takes one or more coroutines or tasks and returns once they are done. A list of return values is then returned from each provided task.
A "return_exceptions" argument can be provided and set to True. This will cause the asyncio.gather() function to catch any CancelledError exception raised by tasks and return them in the list of return values.
For example:
...
# suppress CancelledError exception
_ = asyncio.gather(task, return_exceptions=True)
This provides a simple one-liner for handling a CancelledError exception in a task.
You can learn more about how to use the asyncio.gather() function in the tutorial:
Suppress CancelledError with contextlib.suppress()
We can suppress CancelledError exceptions using contextlib.suppress().
The contextlib.suppress() is a context manager that takes one or more exceptions as arguments and explicitly suppresses them within the body of the context manager.
Return a context manager that suppresses any of the specified exceptions if they occur in the body of a with statement and then resumes execution with the first statement following the end of the with statement.
-- contextlib — Utilities for with-statement contexts
We can use this context manager when awaiting a canceled task instead of a try-except block.
For example:
...
# await the cancelled task
with contextlib.suppress(asyncio.CancelledError):
await task
This is a preferred approach to suppress the CancelledError exception if the caller has no plans to do anything with the exception, e.g. does not plan to log or do cleanup.
Danger of Suppressing CancelledError
Suppressing CancelledError exceptions can be dangerous.
Specifically, suppressing CancelledError within a target task that has been canceled is discouraged by the asyncio documentation.
... suppressing cancellation completely is not common and is actively discouraged.
-- Coroutines and Tasks
This is because the expectation of the cancel() method called on a task is that the target task will raise a CancelledError exception.
If this does not occur, then the expectation or contract with the cancel() method has been broken.
Generally, suppressing the CancelledError exception should occur outside of the target task that is being handled.
The most common point for handling and suppressing a CancelledError exception is at the scope where the task was requested to cancel, e.g. where the cancel() method was called.
Now that we know how to suppress CancelledError exceptions, let's look at some worked examples.
Example Suppressing CancelledError Within Task
We can explore an example of how to suppress a CancelledError exception from within the task.
In this example, we will define a task that takes a long time to complete and then report a message. The task body is protected with a try-except for the CancelledError. The main coroutine schedules the task to run in the background, sleeps a while to allow the task to complete, then cancels the task and waits for the task to cancel before reporting a final message.
Firstly, we can define the long-running task that suppresses the CancelledError when it is canceled.
# task that take a long time
async def work():
try:
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
except asyncio.CancelledError:
# suppress CancelledError
pass
Next, we can define the main coroutine that schedules the work() task, allows it to run, and then cancels it.
# main coroutine
async def main():
# create and schedule the task
task = asyncio.create_task(work())
# allow the task to start
await asyncio.sleep(2)
# cancel the task
task.cancel()
# await the task
await task
# report a final message
print('Main is done')
Finally, we can start the event loop and execute the main() coroutine.
...
# start the event loop
asyncio.run(main())
Tying this together, the complete example is listed below.
# SuperFastPython.com
# example of suppressing CancelledError within the task
import asyncio
# task that take a long time
async def work():
try:
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
except asyncio.CancelledError:
# suppress CancelledError
pass
# main coroutine
async def main():
# create and schedule the task
task = asyncio.create_task(work())
# allow the task to start
await asyncio.sleep(2)
# cancel the task
task.cancel()
# await the task
await task
# report a final message
print('Main is done')
# start the event loop
asyncio.run(main())
Running the example first starts the event loop and then runs the main() coroutine.
The main() coroutine runs and schedules the work() coroutine as a task.
It then suspends for 2 seconds, allowing the work task to run.
The work() task runs and suspends with a 10-second sleep.
The main() coroutine resumes and cancels the work() task, it then suspends and waits for the work() task to be done.
A CancelledError exception is raised within the work() task. It is handled and no action is taken. The work() task is then terminated.
The main() coroutine resumes and reports a final message.
This highlights how we can suppress a CancelledError exception from within the canceled task.
Main is done
Next, let's look at an example of suppressing the CancelledError exception at the point where the target task is canceled.
Example Suppressing CancelledError Within Caller
We can explore an example of how to suppress a CancelledError exception from the caller of the task that requested the task to be canceled.
In this case, we can update the above example so that the work() coroutine no longer handles the CancelledError exception.
# task that take a long time
async def work():
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
We can then update the main() coroutine so that when the canceled task is awaited, it handles an expected CancelledError exception, which is then ignored.
...
# cancel the task
task.cancel()
try:
# await the task
await task
except asyncio.CancelledError:
# suppress CancelledError
pass
Tying this together, the complete example is listed below.
# SuperFastPython.com
# example of suppressing CancelledError in caller
import asyncio
# task that take a long time
async def work():
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
# main coroutine
async def main():
# create and schedule the task
task = asyncio.create_task(work())
# allow the task to start
await asyncio.sleep(2)
# cancel the task
task.cancel()
try:
# await the task
await task
except asyncio.CancelledError:
# suppress CancelledError
pass
# report a final message
print('Main is done')
# start the event loop
asyncio.run(main())
Running the example first starts the event loop and then runs the main() coroutine.
The main() coroutine runs and schedules the work() coroutine as a task.
It then suspends for 2 seconds, allowing the work task to run.
The work() task runs and suspends with a 10-second sleep.
The main() coroutine resumes and cancels the work() task, it then suspends and waits for the work() task to be done.
A CancelledError exception is raised within the work() and bubbles up, causing the task to terminate.
The main() coroutine resumes and the CancelledError exception is raised. It is then handled and suppressed.
Finally, the main() coroutine reports a final message.
This highlights how we can suppress a CancelledError exception from the caller that canceled the target task.
Main is done
Next, let's look at an example of suppressing a CancelledError exception using asyncio.gather().
Example Suppressing CancelledError via asyncio.gather()
We can explore an example of how to suppress a CancelledError exception using the asyncio.gather() function.
In this case, we can update the above example so that instead of explicitly awaiting the canceled task, the main() coroutine can await a call to asyncio.gather() that takes the canceled task as an argument and is configured to return exceptions.
...
# suppress CancelledError
_ = asyncio.gather(task, return_exceptions=True)
Tying this together, the complete example is listed below.
# SuperFastPython.com
# example of suppressing CancelledError with asyncio.gather()
import contextlib
import asyncio
# task that take a long time
async def work():
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
# main coroutine
async def main():
# create and schedule the task
task = asyncio.create_task(work())
# allow the task to start
await asyncio.sleep(2)
# cancel the task
task.cancel()
# suppress CancelledError
_ = asyncio.gather(task, return_exceptions=True)
# report a final message
print('Main is done')
# start the event loop
asyncio.run(main())
Running the example first starts the event loop and then runs the main() coroutine.
The main() coroutine runs and schedules the work() coroutine as a task.
It then suspends for 2 seconds, allowing the work task to run.
The work() task runs and suspends with a 10-second sleep.
The main() coroutine resumes and cancels the work() task, it then suspends and waits for the work() task to be done via asyncio.gather().
A CancelledError exception is raised within the work() and bubbles up, causing the task to terminate.
The main() coroutine resumes and the CancelledError exception is raised in the asyncio.gather() function. It is then handled, suppressed, and returned.
Finally, the main() coroutine reports a final message.
This highlights how we can suppress a CancelledError exception from the caller via asyncio.gather() function.
Main is done
Next, let's look at an example of suppressing a CancelledError exception using contextlib.suppress().
Example Suppressing CancelledError via contextlib.suppress()
We can explore an example of how to suppress a CancelledError exception using the contextlib.suppress() context manager.
In this case, we can update the above example to use the contextlib.suppress() context when awaiting the canceled task.
...
# suppress CancelledError
with contextlib.suppress(asyncio.CancelledError):
# await the task
await task
Tying this together, the complete example is listed below.
# SuperFastPython.com
# example of suppressing CancelledError with contextlib.suppress
import contextlib
import asyncio
# task that take a long time
async def work():
# sleep a long time
await asyncio.sleep(10)
# report a final message
print('Task sleep completed normally')
# main coroutine
async def main():
# create and schedule the task
task = asyncio.create_task(work())
# allow the task to start
await asyncio.sleep(2)
# cancel the task
task.cancel()
# suppress CancelledError
with contextlib.suppress(asyncio.CancelledError):
# await the task
await task
# report a final message
print('Main is done')
# start the event loop
asyncio.run(main())
Running the example first starts the event loop and then runs the main() coroutine.
The main() coroutine runs and schedules the work() coroutine as a task.
It then suspends for 2 seconds, allowing the work task to run.
The work() task runs and suspends with a 10-second sleep.
The main() coroutine resumes and cancels the work() task, it then suspends and waits for the work() task to be done via asyncio.gather().
A CancelledError exception is raised within the work() and bubbles up, causing the task to terminate.
The main() coroutine resumes and the CancelledError exception is raised and then suppressed by the contextlib.suppress() context manager.
The main() coroutine resumes and exits the context manager and then reports a final message.
This highlights how we can suppress a CancelledError exception from the caller via the contextlib.suppress() context manager.
Main is done
Takeaways
You now know how to suppress CancelledError exceptions in asyncio programs.
If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.