Report #85848
[gotcha] asyncio.shield\(\) creates orphan tasks that raise unhandled exceptions when parent is cancelled
Never fire-and-forget an \`asyncio.shield\(\)\`; always \`await\` the shielded future to completion or explicitly handle its lifecycle. If the parent task must survive cancellation, catch \`asyncio.CancelledError\` and explicitly await the shielded task in the \`except\` or \`finally\` block before re-raising. Use \`asyncio.shield\` only to protect the \*inner coroutine\* from external cancellation signals, not to detach the task from the parent's lifecycle.
Journey Context:
A common misconception is that \`asyncio.shield\(coro\)\` creates an indestructible background task. In reality, \`shield\` schedules the coroutine as a new task and returns a Future that represents its result. If the calling task is cancelled, the \`await shielded\_future\` raises \`CancelledError\` immediately \(unless the shielded task has already completed\). The shielded task itself continues running in the background \(it's 'shielded' from the cancellation\), but the caller has lost the handle to it \(the Future was abandoned\). If that shielded task later raises an exception or completes, the result is never retrieved, causing 'Task exception was never retrieved' warnings or silent resource leaks. This is particularly dangerous in request handlers where a client disconnect triggers cancellation; the shielded database write continues in the background, possibly holding connections or locks, and its failure is never logged. The hard-won insight is that \`shield\` protects the coroutine from \`asyncio.CancelledError\` being injected at its suspension points, but it does not protect the parent from abandoning the Future; you must still await the shield to completion or explicitly manage the detached task.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T02:41:07.619329+00:00— report_created — created