Python Cancelled Error: Handling Task Cancellation
python cancelled error: Understand Python's CancelledError, when it's raised, and how to handle it gracefully in asyncio tasks without breaking cleanup or swallowing e...
When working with asyncio, the python cancelled error—asyncio.CancelledError—is a special exception that signals a task has been requested to stop. Unlike most exceptions, it inherits from BaseException rather than Exception, which changes how it propagates through your code. Handling it correctly is essential for writing robust concurrent applications.
What Is asyncio.CancelledError and When Does It Occur?
asyncio.CancelledError is raised when a task's cancellation is requested. The most common trigger is calling task.cancel() on a Task object. It also occurs when a coroutine is cancelled by asyncio.wait_for() after a timeout, or when a parent task is cancelled and its child tasks are automatically cancelled.
The exception is thrown at the point where the coroutine is currently awaiting. If the coroutine is blocked on await asyncio.sleep(), the sleep is interrupted and the exception is raised at that await expression. This mechanism allows the event loop to interrupt long-running operations without terminating the entire process.
Why CancelledError Inherits from BaseException
In Python, BaseException is the root class for all exceptions, while Exception is the base for most user-defined errors. CancelledError inherits from BaseException directly, not from Exception. This distinction is deliberate: it ensures that a try/except Exception block does not accidentally catch cancellation. If cancellation were a normal exception, a broad except Exception handler might swallow it, preventing the task from actually stopping. By making it a BaseException, cancellation propagates even through code that catches general exceptions, unless explicitly handled.
This behavior is important when writing libraries or framework code that must not interfere with task cancellation. For example, a cleanup handler that catches Exception to log errors will not intercept CancelledError, allowing the cancellation to continue upward.
Graceful Cleanup with try/finally
When a task is cancelled, you often need to release resources, close connections, or cancel sub-tasks. The try/finally construct guarantees that cleanup code runs whether the coroutine completes normally, raises an exception, or is cancelled.
import asyncio async def worker(): try: # Simulate a long-running operation await asyncio.sleep(10) finally: # This runs even if CancelledError is raised print("Cleaning up resources")
In this example, if the task is cancelled while sleeping, the finally block executes before the CancelledError propagates. This is the safest way to ensure cleanup without accidentally swallowing the cancellation. The exception continues to propagate after the finally block finishes, so the task is properly marked as cancelled.
Catching CancelledError for Cleanup and Re-raising
Sometimes you need to perform specific actions when a cancellation occurs, such as logging or sending a cancellation signal to an external service. In that case, you can catch CancelledError, do the work, and then re-raise it to preserve the cancellation semantics.
async def worker(): try: await asyncio.sleep(10) except asyncio.CancelledError: # Perform custom cleanup print("Cancelled, cleaning up") raise # Re-raise to propagate cancellation
The raise statement without an argument re-raises the current exception. This is critical: if you catch CancelledError and do not re-raise, the task will appear to complete normally instead of being cancelled. That can cause subtle bugs in code that relies on task cancellation status, such as asyncio.gather(return_exceptions=True) or task.cancelled().
Preventing Cancellation with asyncio.shield()
Sometimes you want to protect a coroutine from being cancelled by its caller. For example, you may be in the middle of a critical database write that should finish even if the surrounding task is cancelled. asyncio.shield() creates a new task that runs the coroutine independently, and the await on the shield raises CancelledError if the outer task is cancelled, but the inner task continues.
async def main(): task = asyncio.create_task(asyncio.sleep(10)) try: await asyncio.shield(task) except asyncio.CancelledError: print("Main cancelled, but task continues") raise
In this example, if main is cancelled while awaiting the shield, the CancelledError is raised at the await asyncio.shield(task) line. The inner task, however, is not cancelled and continues running in the background. This is useful for operations that must complete regardless of the caller's cancellation. Be cautious: shielding a task means it will outlive the parent, so you must ensure it eventually completes or is cancelled explicitly to avoid resource leaks.
Common Pitfalls with CancelledError
One of the most frequent mistakes is swallowing CancelledError without re-raising it. This happens when a broad except BaseException or except Exception (though the latter won't catch it) is used, or when a try/except block catches it and then returns normally. The task then finishes successfully, and any code that expected cancellation will misbehave.
Another pitfall is catching BaseException to handle CancelledError but also catching KeyboardInterrupt and SystemExit. While that works, it often masks other critical errors. Prefer catching asyncio.CancelledError explicitly.
A related issue is relying on finally to perform cleanup but then accidentally raising a different exception in the finally block, which can replace the original CancelledError. If you must raise an exception during cleanup, consider using contextlib.suppress or logging instead of raising.
Testing Cancellation Behavior
Testing cancellation is straightforward with asyncio.wait_for() or by manually calling task.cancel().
import asyncio async def test_cancellation(): async def long_task(): try: await asyncio.sleep(100) finally: print("Cleanup") task = asyncio.create_task(long_task()) await asyncio.sleep(0.1) task.cancel() try: await task except asyncio.CancelledError: print("Task was cancelled")
In this test, the task is cancelled after a short delay. The finally block runs, and the CancelledError is caught by the awaiting code. You can also use asyncio.wait_for(task, timeout) to trigger cancellation automatically when the timeout expires. When writing tests, verify that cleanup ran and that the task's cancelled() method returns True after cancellation.
Cancellation in Other Contexts
The term "cancelled error" also appears in concurrent.futures, where Future.cancel() raises CancelledError if the future is already running. However, that exception is different from asyncio.CancelledError and is not part of the asyncio module. In concurrent.futures, you typically check future.cancelled() rather than catching an exception. For asyncio code, the CancelledError is the primary mechanism for cooperative cancellation.
When building libraries that use asyncio, always document how your API handles cancellation. If your function creates child tasks, decide whether they should be cancelled when the parent is cancelled. Using asyncio.shield() for critical sections and re-raising CancelledError after cleanup helps maintain predictable behavior. This attention to cancellation semantics keeps concurrent code maintainable and avoids subtle bugs in production.
Properly handling the python cancelled error is not just about avoiding crashes; it is about respecting the cooperative cancellation model that asyncio provides. By using try/finally for cleanup, re-raising after catching, and shielding when necessary, you ensure that tasks stop promptly and resources are released reliably.