How does asyncio.gather report an exception raised by one of its awaitables?
answer
- Two very different failure modes
- One keyword changes all of it
- Argument order, not completion order
- The siblings are not touched when one fails
basics
~20 sBy default asyncio.gather propagates the first exception out of the await immediately, while the other awaitables keep running untouched. With return_exceptions=True it instead waits for everything and returns exception objects in the results list, positionally.
solid answer
~40 s`asyncio.gather(*aws)` runs its awaitables concurrently and returns their results as a list in **argument order**, not completion order. With the default `return_exceptions=False`, the first awaitable to raise propagates that exception out of the `await` at once — but the siblings are **not** cancelled; they keep running as orphans, and the results of the ones that already succeeded are lost. With `return_exceptions=True`, gather always waits for every awaitable and puts each exception object into the results list at that awaitable's position, so you inspect entries with `isinstance(item, Exception)`. Cancelling the gather itself does cancel every child that has not finished. So the choice is between fail-fast-and-leak and collect-everything: if you need failure to stop the siblings, you must hold the Task objects and cancel them yourself, or use a structured-concurrency group.
code
python · 22 linesimport asyncio
async def pull(depot):
await asyncio.sleep(0.01)
return f"{depot}: 41 units"
async def pull_broken():
await asyncio.sleep(0.01)
raise RuntimeError("stale cached count")
async def main():
results = await asyncio.gather(
pull("depot-1"), pull_broken(), pull("depot-3"), return_exceptions=True
)
for item in results:
print(repr(item))
asyncio.run(main())go deeper
Know that gather runs awaitables concurrently and gives you a list of results lined up with the arguments you passed. Remember that by default one failure ends the await.
Explain both modes precisely: default propagates the first exception and leaves siblings running, return_exceptions=True waits for all and returns exception objects positionally. Be ready to write the isinstance filter.
Demonstrate that you have been bitten: orphaned siblings still writing after the handler ran, lost partial results, never-retrieved-exception noise, and how holding the Task objects yourself fixes all three.
Own the policy: where partial success is a legitimate outcome and gather with return_exceptions is right, versus where all-or-nothing semantics must be enforced by a scoped construct so no orphan can ever outlive its request.
### What gather is `asyncio.gather(*aws)` takes awaitables, schedules any bare coroutines as Tasks, and returns a single awaitable that completes when all of them have. Its return value is a **list of results in the positional order of the arguments** — this is the property people most often rely on, and it is the reason gather is preferred over `asyncio.wait` when you want "all of these, correlated with what I asked for". Completion order is irrelevant to the output; the third argument's result is always at index 2. Consider an inventory sync fanning out to several depots. Two return counts, one blows up on a stale cached value. What you see depends entirely on one keyword. ### Default: return_exceptions=False The gather future is marked with the **first** exception raised by any child, and that exception propagates out of your `await` immediately — you do not wait for the slower siblings. Three consequences follow, and interviewers probe all three: 1. **The siblings are not cancelled.** They continue running on the loop. If they were doing writes, those writes still happen after your error handler has already run. This is the single most common surprise, and it is exactly why structured concurrency exists. 2. **Successful results are lost.** gather raises instead of returning, so the counts the healthy depots already produced are unreachable — unless you kept the Task objects yourself and can read `task.result()` off them afterwards. 3. **Only the first exception is seen.** Later failures in the siblings are never surfaced through gather. If nobody ever retrieves them, you get "Task exception was never retrieved" noise from the loop when those tasks are eventually collected. ### return_exceptions=True Now gather never raises on behalf of a child. It waits for every awaitable, and each exception object is placed in the results list at its argument position, treated "the same as a successful result". The caller does the sorting: ```python results = await asyncio.gather(*calls, return_exceptions=True) for depot, item in zip(depots, results): if isinstance(item, Exception): log_failure(depot, item) else: apply(depot, item) ``` That positional correlation is why gather is such a natural fit for fan-out over a known list of inputs. The price is that nothing fails fast: a partial outage means you wait for the slowest member of the fan-out before you learn anything. ### Cancellation, in both directions The two directions behave differently and both come up: * **Cancelling the gather** (for example because the coroutine awaiting it is cancelled) cancels every child that has not yet completed. This is the one case where gather does propagate cancellation downward. * **A child being cancelled** is treated as that child raising `CancelledError`. With the default flag that `CancelledError` comes out of the gather; with `return_exceptions=True` it appears as an item in the results list. Importantly, the gather call itself is *not* considered cancelled just because one child was. A subtle corollary: `return_exceptions=True` means the results list can contain a `CancelledError`, so `isinstance(item, Exception)` will miss it — `CancelledError` derives from `BaseException`. Check for `BaseException` if cancellation is possible in your fan-out. ### Making failure stop the siblings If you want first-failure to actually halt the rest, gather alone will not do it. The explicit pattern is to create the tasks yourself, gather them, and in a `finally` cancel any task that is not done before awaiting them out — which is verbose enough that the language grew a dedicated construct for scoped children and aggregated errors. Reach for that construct when "all or nothing" is the requirement, and keep gather for the fan-out where partial success is meaningful and you want the results aligned with the inputs. ### Two smaller facts worth having `asyncio.gather()` with no arguments returns an already-completed future whose result is an empty list, so fanning out over an empty input list is safe and needs no special case. And gather accepts bare coroutines as well as Tasks and Futures — it wraps them for you — which is a real difference from `asyncio.wait`, where passing a coroutine has been an error since 3.12. ### The interview summary One sentence: *gather returns results positionally, fails fast by default without cancelling anything, and with `return_exceptions=True` waits for everyone and hands you exceptions as values.* Everything else follows from that.
- With return_exceptions=True, what appears in the results list if one of the children is cancelled?The `CancelledError` is treated like any other exception and lands in the list at that child's position; the gather call itself is not considered cancelled. Watch the type: `CancelledError` inherits from `BaseException`, not `Exception`, so the usual `isinstance(item, Exception)` filter silently misclassifies it as a success. Filter on `BaseException` when cancellation is possible.
- How do you keep the successful results when the default gather raises?Create the tasks yourself before gathering, so you still hold handles after the exception propagates. Then each task can be interrogated with `done()`, `result()` and `exception()` — the successes are readable, and retrieving the failures also silences the "exception was never retrieved" warnings. Passing bare coroutines to gather throws that opportunity away, because you never see the Tasks it created.
- Does asyncio.gather preserve completion order in its results?No — the list is in the order the awaitables were passed, regardless of who finished first. That positional correlation is gather's main ergonomic advantage: you can `zip` the results back against the inputs. If you need results as they arrive rather than aligned with inputs, gather is the wrong tool.
saying these in an interview costs you the question
- Thinks gather cancels the other awaitables when one fails
- Expects results in completion order
- Believes return_exceptions=True suppresses or logs errors
- Assumes gather aggregates every failure by default
- Filters results with isinstance(item, Exception) where cancellation is possible