asyncio.gather и исключения: что я понял не сразу
Долгое время я был уверен: если одна из корутин в asyncio.gather падает, остальные отменяются. Оказалось, нет, и это объяснило пару странных багов с «призрачными» запросами к внешнему API.
Как ведёт себя gather
Без return_exceptions первое исключение пробрасывается наружу из await gather(...). Но остальные задачи продолжают работать в фоне: их никто не отменял. Если это HTTP-запросы, они дойдут до сервера, а результат просто потеряется.
С return_exceptions=True исключения возвращаются в списке результатов как обычные значения. Это удобно, но легко забыть проверить каждый элемент.
TaskGroup
С Python 3.11 есть asyncio.TaskGroup, и для большинства случаев я перешёл на него. Если одна задача падает, группа отменяет остальные, дожидается их и выбрасывает ExceptionGroup со всеми ошибками.
async def load_dashboard(user_id: int):
async with asyncio.TaskGroup() as tg:
user = tg.create_task(fetch_user(user_id))
orders = tg.create_task(fetch_orders(user_id))
return user.result(), orders.result()
Ошибки удобно разбирать через except*: он ловит часть исключений из группы по типу.
try:
data = await load_dashboard(42)
except* httpx.TimeoutException as eg:
log.warning("таймаутов: %d", len(eg.exceptions))
Когда gather всё ещё уместен
Когда задачи независимы и нужно получить всё, что получилось, даже если часть упала: например, опросить десяток источников и показать то, что ответило. Тогда gather(..., return_exceptions=True) с явной проверкой результатов — нормальный выбор.