Async/await в forEach: как подружить асинхронность с циклами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваш async/await молчит в forEach? Разбираемся с первого взгляда

Вы когда-нибудь писали что-то вроде этого кода, надеясь, что он выполнит асинхронные операции по порядку?

const users = ['alice', 'bob', 'charlie'];

users.forEach(async (user) => {
  const data = await fetch(`https://api.example.com/users/${user}`);
  console.log(await data.json());
});

console.log('Готово!');

И получали "Готово!" раньше, чем загружались данные? Знакомо? Вы не одиноки. Это один из самых популярных вопросов на Stack Overflow с тысячами просмотров. Проблема в том, что forEach — это синхронный метод. Он просто запускает коллбэки, но не ждёт их завершения. Ваши async-функции стартуют, но forEach уже мчится дальше, как спринтер, которому плевать на финиш остальных.

В этой статье мы не просто покажем, как это исправить. Мы разберём почему так происходит, и дадим вам три рабочих решения с примерами. Вы узнаете, когда использовать Promise.all, а когда — старый добрый for...of. Поехали!

Анатомия проблемы: forEach и обещания

Давайте заглянем под капот. Метод forEach в JavaScript устроен так:

Array.prototype.forEach = function(callback) {
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this);
  }
};

Видите? Он просто вызывает callback для каждого элемента, но ничего не возвращает. Даже если коллбэк возвращает Promise, forEach его игнорирует. Он не ждёт, не собирает результаты — он просто запускает и забывает.

Когда вы пишете async (user) => { ... }, ваша функция возвращает Promise. Но forEach не знает, что с ним делать. В итоге все запросы запускаются параллельно (если это fetch), но вы не можете дождаться их завершения. Код после forEach выполняется мгновенно.

Реальный пример: как это выглядит на практике

Представьте, что вы обрабатываете массив ID товаров и для каждого нужно получить цену из базы данных:

const productIds = [101, 102, 103];

async function getPrice(id) {
  // Имитация запроса к БД
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Цена для ${id}: $${Math.floor(Math.random() * 100)}`);
    }, 1000);
  });
}

productIds.forEach(async (id) => {
  const price = await getPrice(id);
  console.log(price);
});

console.log('Все цены загружены!'); // Выполнится сразу, а не через 3 секунды

Результат в консоли:

Все цены загружены!
Цена для 101: $42
Цена для 102: $78
Цена для 103: $15

Согласитесь, это не то, что мы ожидали. Хорошая новость: есть простые и элегантные решения.

Решение №1: for...of — последовательное выполнение

Самый простой способ заставить async/await работать в цикле — использовать for...of. В отличие от forEach, for...of работает с async/await нативно, потому что вы управляете потоком выполнения внутри блока.

const productIds = [101, 102, 103];

async function getPrice(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Цена для ${id}: $${Math.floor(Math.random() * 100)}`);
    }, 1000);
  });
}

async function loadAllPrices() {
  for (const id of productIds) {
    const price = await getPrice(id);
    console.log(price);
  }
  console.log('Все цены загружены!');
}

loadAllPrices();

Теперь результат будет таким:

Цена для 101: $42
(через 1 секунду)
Цена для 102: $78
(через 1 секунду)
Цена для 103: $15
(через 1 секунду)
Все цены загружены!

Плюсы: простота, читаемость, предсказуемый порядок.

Минусы: медленно — каждый запрос ждёт завершения предыдущего. Если у вас 1000 элементов, это займет 1000 секунд.

Решение №2: Promise.all — параллельное выполнение

Если порядок не важен, а важна скорость — используйте Promise.all в связке с map. Это запустит все запросы одновременно и дождётся их всех.

const productIds = [101, 102, 103];

async function getPrice(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Цена для ${id}: $${Math.floor(Math.random() * 100)}`);
    }, 1000);
  });
}

async function loadAllPrices() {
  const promises = productIds.map(id => getPrice(id)); // Создаём массив промисов
  const prices = await Promise.all(promises);          // Ждём все сразу
  
  prices.forEach(price => console.log(price));
  console.log('Все цены загружены!');
}

loadAllPrices();

Результат (все три цены появятся одновременно через ~1 секунду):

Цена для 101: $42
Цена для 102: $78
Цена для 103: $15
Все цены загружены!

Плюсы: максимальная скорость, идеально для независимых запросов.

Минусы: если один промис упадёт с ошибкой — упадёт весь Promise.all. Для обработки ошибок используйте Promise.allSettled.

Бонус: Promise.allSettled для отказоустойчивости

Если вы хотите, чтобы один неудачный запрос не убивал весь процесс:

const results = await Promise.allSettled(
  productIds.map(id => getPrice(id))
);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.error('Ошибка:', result.reason);
  }
});

Решение №3: Reduce с аккумулятором промисов

Это более продвинутый метод, который даёт вам контроль над последовательностью, но при этом позволяет обрабатывать результаты по мере выполнения. Подходит, когда нужно выполнять запросы строго по порядку, но с гибкостью.

const productIds = [101, 102, 103];

async function getPrice(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Цена для ${id}: $${Math.floor(Math.random() * 100)}`);
    }, 1000);
  });
}

productIds.reduce(async (previousPromise, id) => {
  await previousPromise; // Ждём завершения предыдущего
  const price = await getPrice(id);
  console.log(price);
}, Promise.resolve());

Этот код работает как цепочка: каждый следующий запрос начинается только после завершения предыдущего. Promise.resolve() — это стартовый промис, который сразу выполняется.

Плюсы: последовательное выполнение + возможность добавить логику между шагами.

Минусы: менее читаемо

Блоги

Рекомендаций по книгам