Avatar ·

Как правильно использовать async/await внутри цикла forEach в JavaScript?

Всем привет! Я начал изучать асинхронный JavaScript и столкнулся с проблемой, которая меня полностью поставила в тупик. У меня есть массив URL-адресов, и я хочу последовательно загрузить данные с каждого из них, используя `fetch` и `async/await`. Я написал такой код: ```javascript const urls = ['https://api.example.com/data/1', 'https://api.example.com/data/2', 'https://api.example.com/data/3']; async function fetchUrls() { urls.forEach(async (url) => { const response = await fetch(url); const data = await response.json(); console.log('Загружено:', data); }); console.log('Все запросы отправлены!'); } fetchUrls(); ``` Я ожидал, что сначала загрузятся данные с первого URL, потом со второго, потом с третьего, и только после этого выведется сообщение "Все запросы отправлены!". Но на практике я вижу, что сообщение выводится сразу же, а данные приходят в произвольном порядке, часто одновременно. Я уже прочитал, что `forEach` не работает с `async/await`, но не понимаю почему. Ведь я же написал `async` перед callback-функцией. Может быть, я что-то упускаю? Как правильно организовать последовательную загрузку данных с ожиданием каждого запроса? Буду благодарен за объяснение с примерами правильного кода. Заранее спасибо!<<>>

Почему 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 для асинхронных операций постепенно уходит в прошлое, но всё ещё может быть полезен в некоторых сценариях.