Skip to main content

Clean up child tasks when an asyncio operation fails

2026-11-11 15:57:29.459312+00:00
Clean up child tasks when an asyncio operation fails

When several asynchronous operations belong to one result, their lifetimes should end together. If a required operation fails while its siblings keep running, they can hold connections or continue producing output after the caller has already received an error.

Task group with one error and cancellation arrows to siblings.

Give related work one lifetime

The following synthetic example requires two pieces of a report. One deliberately fails; the other owns a resource that must be cleaned up.

import asyncio

async def load_details(events):
    events.append("details-opened")
    try:
        await asyncio.sleep(10)
        return {"name": "fixture"}
    finally:
        events.append("details-closed")

async def load_totals():
    await asyncio.sleep(0)
    raise ValueError("synthetic totals failure")

async def main():
    events = []
    try:
        async with asyncio.TaskGroup() as group:
            group.create_task(load_details(events))
            group.create_task(load_totals())
    except* ValueError as errors:
        assert len(errors.exceptions) == 1

    assert events == ["details-opened", "details-closed"]
    print(events)

asyncio.run(main())

The resource-owning coroutine uses finally so its cleanup runs when cancellation arrives. A real implementation would close a response stream, return a connection or remove temporary state in that block.

The exception handler uses Python's exception-group syntax because a group can report failures from more than one task. Catch the failures the application knows how to handle. An unrelated programming error should remain visible instead of being converted into an apparently successful empty report.

Keep cancellation meaningful

Cancellation is cooperative. It is delivered when the coroutine reaches a point where it can suspend. A long blocking function can delay it; moving suitable blocking work outside the event loop may be necessary.

Do not swallow a cancellation exception and continue ordinary work. Cleanup can run before re-raising the exception. A coroutine that suppresses cancellation changes the lifetime contract for the entire group and can delay the caller's exit.

A task group also cannot undo a remote action that completed before another task failed. If two concurrent operations send messages or change provider state, cancellation does not form a distributed transaction. Prefer reversible preparation work in the group, followed by an explicit commit or approval step where the application requires one.

Cleanup should also tolerate a partially acquired resource. An exception can occur before the handle or connection has been assigned.

This pattern applies to the Python orchestration concerns described in Postzyo. Python's TaskGroup documentation defines the failure and cancellation behavior.