This rule raises an issue when a cancellation excception is caught without re-raising it.

Why is this an issue?

Asynchronous frameworks like asyncio, trio, and anyio use special exceptions to signal that a task or operation should be cancelled. These exceptions are not typical errors indicating a logical flaw in the task but are directives for the task to terminate its execution prematurely and perform necessary cleanup.

When a task is cancelled, the framework typically injects this cancellation exception into it. The task is expected to:

If a cancellation exception is caught and not re-raised (e.g., it’s suppressed with a pass statement, only logged, the handler returns normally, or a different exception is raised instead), the cancellation signal is effectively "swallowed".

This prevents the framework and any calling code from knowing that the task has acknowledged the cancellation and is stopping. The task might even continue running parts of its code after the except block, which is contrary to the purpose of cancellation.

Properly propagating cancellation exceptions is crucial for the cooperative multitasking model these frameworks rely on.

What is the potential impact?

Suppressing cancellation exceptions can lead to significant problems:

How to fix it in Asyncio

If you need to catch cancellation exceptions for cleanup purposes, make sure to re-raise them after your cleanup code.

Alternatively, you could add a specific handler for cancellation exceptions before your general exception handler.

Code examples

Noncompliant code example

import asyncio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except asyncio.CancelledError:
        return  # Noncompliant

Compliant solution

import asyncio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except asyncio.CancelledError:  # Compliant
        raise

How to fix it in Trio

If you need to catch cancellation exceptions for cleanup purposes, make sure to re-raise them after your cleanup code.

Alternatively, you could add a specific handler for cancellation exceptions before your general exception handler.

Code examples

Noncompliant code example

import trio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except trio.Cancelled:  # Noncompliant
        return

Compliant solution

import trio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except trio.Cancelled:  # Compliant
        raise

How to fix it in AnyIO

If you need to catch cancellation exceptions for cleanup purposes, make sure to re-raise them after your cleanup code.

Alternatively, you could add a specific handler for cancellation exceptions before your general exception handler.

Code examples

Noncompliant code example

import anyio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except anyio.get_cancelled_exc_class():  # Noncompliant
        return

Compliant solution

import anyio

async def compute_result(data): ...

async def process_data(data):
    try:
        result = await compute_result(data)
        return result
    except anyio.get_cancelled_exc_class():  # Compliant
        raise

Pitfalls

Asynchronous cleanup operations in except CancelledError or finally blocks can themselves be interrupted by cancellation. While asyncio.shield() (or library equivalents) can protect critical cleanup code, use it sparingly as it may delay shutdown.

Resources

Documentation