Как переписать цикл с асинхронными операциями с использованием await
Что проверяют
Понимаете ли вы, что делает await внутри разных видов циклов и когда операции должны идти последовательно, а когда параллельно. Следом почти всегда спрашивают, почему не работает forEach, и просят переписать код на Promise.all с обработкой ошибок.
Как отвечать
Сначала назовите проблему. forEach, map и filter не ждут промис, который вернул колбэк: arr.forEach(async (x) => { await save(x) }) запустит все вызовы сразу, а код после цикла выполнится до их завершения. Ошибки из таких колбэков не попадут во внешний try/catch.
Дальше варианты под задачу:
- последовательно:
for...ofили обычныйforсawaitвнутри. Нужен, когда следующий шаг зависит от предыдущего или важен порядок, например пагинация по курсору. Общее время равно сумме всех запросов; - параллельно:
await Promise.all(items.map((x) => load(x))). Время примерно равно самому долгому запросу, результаты приходят в порядке исходного массива. Первый же reject отклоняет весьPromise.all, но остальные запросы при этом не отменяются; - параллельно с частичными ошибками:
Promise.allSettled, дальше разбираетеstatusкаждого элемента; - с ограничением параллельности: пачками по N, пулом воркеров или библиотекой вроде
p-limit. Так вы не упрётесь в rate limit API; - асинхронные источники:
for await...ofдля асинхронных итераторов, например генератора, который постранично тянет данные.
Отдельно упомяните отмену: передайте AbortController.signal в fetch и прерывайте оставшиеся запросы, если пользователь ушёл со страницы.
Где спотыкаются
Ставят async в колбэк forEach и считают, что цикл стал ждать. Это самая частая ошибка, ради неё вопрос и задают.
Переписывают всё на последовательный for...of без причины. Десять независимых запросов по 300 мс превращаются в три секунды ожидания вместо трёхсот миллисекунд.
Запускают Promise.all на тысячу элементов без ограничения и получают отказы сервера. Заодно забывают, что при одном упавшем запросе результаты остальных из Promise.all уже не получить.