Skip to content

← Concurrency with Threads and asyncio step 30 of 56

Hard Primitives

Run Async Cleanup Without Hiding the Real Error

Write a helper that awaits body() and then always awaits cleanup(), even when the body fails or is cancelled. The caller gets the body's value or its exception. A failing cleanup must leave the body's error in place, and a stuck cleanup must give up after a time limit.

async def with_cleanup[T](
    body: Callable[[], Awaitable[T]],
    cleanup: Callable[[], Awaitable[None]],
    *,
    cleanup_timeout: float,
) -> T

For example, if body() raises ValueError("body failed") and cleanup() then raises RuntimeError, the caller gets the ValueError, with the note "cleanup failed" attached.

What to write

Await body(). Then await cleanup() in the same task, inside asyncio.timeout(cleanup_timeout). Finish according to this table:

If the body... and cleanup finishes in time and cleanup raises or runs out of time
returns a value return that value return that value
raises, or is cancelled re-raise the same exception add one note, then re-raise the same exception

Add the note with exc.add_note(...), using the two strings the starter defines:

  • CLEANUP_TIMED_OUT when your cleanup timer ran out. This includes a cleanup that caught the timer's cancellation and then returned normally.
  • CLEANUP_FAILED when cleanup raised an Exception before your timer ran out. A TimeoutError that cleanup raises by itself counts as a failure.
  • No note when cleanup finished in time.

To tell your timer apart from cleanup's own TimeoutError, keep the timeout in a variable, scope = asyncio.timeout(cleanup_timeout). Then scope.expired() is True only when your timer ran out.

Also follow these rules:

  • Catch BaseException around the body, so cleanup runs after a cancellation too. After cleanup, re-raise the exception with a bare raise.
  • Around cleanup, catch only Exception. If the task is cancelled again during cleanup, that CancelledError must propagate.
  • Run everything in the caller's task. The report counts any extra task that is still running.

What the driver reports

Keep solve as it is. Each scenario picks a body that returns 42 or raises, and a cleanup that succeeds, raises, raises TimeoutError or hangs until your timer cancels it. Some scenarios also cancel the task partway through the body. The report has these keys:

  • log: the steps the body and cleanup ran, in order.
  • outcome: "ok", or the name of the exception the caller saw.
  • message: that exception's message, or "".
  • value: the body's return value, or -1 if the caller saw an exception.
  • notes: the notes on that exception, or [].
  • pending: tasks other than the driver still running at the end. It must be 0.