Asyncio Gather Raises but Other Tasks Continue: Own the Remaining Tasks
By ON Lab · Sources checked November 9, 2026
Separate gather’s first propagated exception from the lifetime of sibling tasks. Keep their handles and decide whether to await completion or cancel and collect them.
Reproduce the boundary in a disposable example
One gather task fails while sibling tasks keep running or appear to stop at program exit.
import asyncio
async def fail():
raise ValueError("sample failure")
async def finish(events):
await asyncio.sleep(0)
events.append("finished")
return 7
async def main():
events = []
tasks = [asyncio.create_task(fail()),
asyncio.create_task(finish(events))]
try:
await asyncio.gather(*tasks)
except ValueError:
results = await asyncio.gather(*tasks, return_exceptions=True)
assert isinstance(results[0], ValueError)
assert results[1] == 7 and events == ["finished"]
asyncio.run(main())
print("sibling completion collected after failure")Read the result before changing application code
The second gather keeps the demonstration alive until both results are collected. Without that wait, returning from main can trigger runner shutdown while siblings are still pending. That can make an observer attribute cancellation to gather itself. Capture both the first error and the final task states when diagnosing real jobs.
Choose the behavior your caller needs
For independent jobs, collecting exceptions as results can be appropriate; inspect each result instead of reporting the whole batch as successful. For a group that must fail together, consider TaskGroup on Python 3.11 or later. For manually owned tasks, cancel unfinished siblings and then await them with return_exceptions=True when failure should stop the group. Cancellation cannot undo external writes already completed.
Scope and limits
This example has deterministic local work and no network or production jobs. TaskGroup exception groups and cancellation cleanup need their own application tests.
Related: compare the adjacent data or execution boundary.
The embedded example was executed in an isolated local Python process on November 9, 2026. It is a reproducible boundary check, not a test of your full application.