Как правильно использовать async/await внутри цикла forEach в JavaScript?
Почему forEach не работает с async/await?
Проблема в том, что метод forEach не ожидает завершения асинхронных операций. Он просто запускает каждую callback-функцию и сразу переходит к следующей итерации, не дожидаясь await. Ваша callback-функция помечена как async, но это не заставляет forEach ждать её завершения — он просто получает Promise и продолжает выполнение.
Фактически, все три запроса запускаются практически одновременно, а сообщение "Все запросы отправлены!" выводится до того, как любой из них завершится.
Решение 1: последовательное выполнение с for...of
Самый простой и понятный способ — использовать цикл for...of с await внутри:
const urls = ['https://api.example.com/data/1', 'https://api.example.com/data/2', 'https://api.example.com/data/3'];
async function fetchUrlsSequentially() {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
console.log('Загружено:', data);
}
console.log('Все запросы выполнены последовательно!');
}
fetchUrlsSequentially();
Здесь каждый следующий запрос начнётся только после завершения предыдущего. await работает внутри цикла, потому что цикл for...of — это обычный синхронный конструкт, который корректно обрабатывает await.
Решение 2: параллельное выполнение с Promise.all
Если вам не нужна последовательность, а важна скорость — запустите все запросы параллельно:
const urls = ['https://api.example.com/data/1', 'https://api.example.com/data/2', 'https://api.example.com/data/3'];
async function fetchUrlsParallel() {
const promises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
const results = await Promise.all(promises);
console.log('Все данные загружены:', results);
}
fetchUrlsParallel();
Метод map создаёт массив промисов, а Promise.all ждёт завершения всех. Это быстрее, но порядок результатов сохраняется.
Решение 3: кастомная асинхронная forEach
Если вам очень хочется использовать именно forEach, можно написать свою версию:
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
async function fetchUrls() {
const urls = ['https://api.example.com/data/1', 'https://api.example.com/data/2', 'https://api.example.com/data/3'];
await asyncForEach(urls, async (url) => {
const response = await fetch(url);
const data = await response.json();
console.log('Загружено:', data);
});
console.log('Все запросы выполнены!');
}
fetchUrls();
Этот вариант даёт тот же эффект, что и for...of, но сохраняет синтаксис, похожий на forEach.
Важные моменты
- Не используйте
forEachсasync/await— это гарантированно приведёт к неожиданному поведению. - Для последовательного выполнения используйте
for...ofилиfor. - Для параллельного выполнения используйте
Promise.allсmap. - Помните об обработке ошибок: оборачивайте код в
try...catch.
async function fetchUrlsSafe() {
try {
for (const url of urls) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Ошибка HTTP: ${response.status}`);
const data = await response.json();
console.log(data);
}
} catch (error) {
console.error('Произошла ошибка:', error);
}
}
Вывод: forEach не предназначен для асинхронных операций. Используйте правильные инструменты для каждой задачи, и ваш код будет работать предсказуемо.
Альтернативный подход: использование reduce для последовательных запросов
Ещё один элегантный способ организовать последовательное выполнение асинхронных операций — использовать метод reduce для "сцепления" промисов:
const urls = ['https://api.example.com/data/1', 'https://api.example.com/data/2', 'https://api.example.com/data/3'];
async function fetchUrlsWithReduce() {
const results = await urls.reduce(async (previousPromise, url) => {
const acc = await previousPromise;
const response = await fetch(url);
const data = await response.json();
acc.push(data);
return acc;
}, Promise.resolve([]));
console.log('Результаты по порядку:', results);
}
fetchUrlsWithReduce();
Как это работает:
Promise.resolve([])— начальное значение, которое сразу резолвится с пустым массивом.- Каждая итерация ждёт завершения предыдущего промиса через
await previousPromise. - Затем выполняется новый запрос, результат добавляется в массив.
- В итоге мы получаем массив данных в правильном порядке.
Этот подход особенно полезен, когда нужно передавать результат предыдущего запроса в следующий (например, при пагинации). Однако для простых случаев for...of остаётся более читаемым и понятным для начинающих.
Также стоит отметить, что с появлением for await...of (для асинхронных итераторов) и современных возможностей JavaScript, reduce для асинхронных операций постепенно уходит в прошлое, но всё ещё может быть полезен в некоторых сценариях.