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.

Medium Python 3.9+ Aug 9, 2026 Concurrency & performance 13 views 0 copies

Python code

28 lines
Python 3.9+
import 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

stdout
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

  1. Use `asyncio.shield()` to protect a section of code from cancellation while cleanup runs
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.