Report #104400
[gotcha] asyncio.CancelledError is a BaseException since Python 3.8, so 'except Exception' does not catch it, but 'except BaseException' can silently suppress cancellation if not re-raised
Always re-raise asyncio.CancelledError after handling it \(e.g., in a finally block or custom shutdown logic\). Use \`except BaseException\` only if you absolutely must catch everything, and then re-raise CancelledError. Prefer \`except\*\` in Python 3.11\+ for exception groups. Do not use bare \`except:\` or \`except BaseException\` in asyncio code unless the caught CancelledError is explicitly re-raised. Consider using \`asyncio.shield\(\)\` to protect critical sections, but note that it does not protect against cancellation of the shield’s own awaitable if it has already started.
Journey Context:
Prior to Python 3.8, asyncio.CancelledError was a subclass of Exception, so \`except Exception\` would catch it. PEP 492 changed it to BaseException to prevent accidental swallowing of cancellation by overly broad exception handlers. This was a good design decision, but it introduced a subtle breaking change: code that previously caught all exceptions and continued would now let CancelledError propagate \(correctly\). However, many old patterns that use \`except Exception\` for general error handling still work for most exceptions, but cancellation becomes a silent issue if a user wrote \`except BaseException\` \(often with \`except:\` synonym\) and did not re-raise. For example, cleanup code in a finally block that catches all exceptions and logs them may inadvertently swallow cancellation, leaving the task in an undefined state. The correct pattern is to catch CancelledError separately \(or as the first except in a chain\) and re-raise it, while handling other exception classes appropriately. The \`asyncio.shield\(\)\` function is often misunderstood: it prevents cancellation of the wrapped awaitable only if the cancellation is requested on the shielded task; if the shielded awaitable itself raises CancelledError \(e.g., because it internally awaited a cancellable operation\), the shield does not suppress that. Documentation and common practice now emphasize re-raising CancelledError in any finally block that handles cleanup and might catch all exceptions.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-08-16T20:03:39.530963+00:00— report_created — created