How to Cancel an asyncio Task with Graceful Cleanup in Python
Cancel a running asyncio task, handle the cancellation signal inside a worker coroutine to perform cleanup, then re-raise so the cancellation propagates correctly.
Python code
28 linesimport asyncio
async def worker(name: str, sleep: float) -> None:
try:
print(f"{name}: starting")
await asyncio.sleep(sleep)
print(f"{name}: completed")
except asyncio.CancelledError:
print(f"{name}: cancelled, cleaning up...")
await asyncio.sleep(0.2) # Simulate cleanup work
print(f"{name}: cleanup done")
raise # Re-raise to propagate cancellation
async def main() -> None:
task = asyncio.create_task(worker("worker-1", 3.0))
await asyncio.sleep(0.5)
print("main: cancelling task")
task.cancel()
try:
await task
except asyncio.CancelledError:
print("main: task was cancelled")
if __name__ == "__main__":
asyncio.run(main())
Output
worker-1: starting
main: cancelling task
worker-1: cancelled, cleaning up...
worker-1: cleanup done
main: task was cancelled
How it works
When you call task.cancel(), asyncio schedules a CancelledError to be thrown into the coroutine at its next await point. Catching that exception inside the worker lets you run cleanup logic before shutting down. After cleanup, you must raise the CancelledError again so the task is marked as cancelled and awaiting it raises the exception. Without the re-raise, the task would appear to complete successfully, hiding the cancellation from the caller. The main function then awaits the task inside a try/except block to confirm and handle the cancellation at the program level.
Common mistakes
- Forgetting to re-raise CancelledError after cleanup, which makes the task look successful
- Running cleanup code that awaits, but blocking the event loop if a new task is created unawaited
- Catching CancelledError with a bare `except` instead of the specific exception type
Variations
- Use `asyncio.shield()` to protect a section of code from cancellation while cleanup runs
- Wrap cleanup in `try/finally` to ensure resources are released even on unexpected errors
Real-world use cases
- Shutting down a web server gracefully and closing database connections on SIGTERM.
- Cancelling a long-running data-fetch job when a user aborts a request, while still flushing partial logs.
- Stopping background worker tasks during application teardown without leaving uncommitted state.
Sponsored
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- Graceful Shutdown Executor Context Manager in Python medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
- How to Demonstrate the GIL with Python Threads vs Processes medium
Keep learning
Related tutorials and quizzes for this topic.