Async/await и Array.map в TypeScript: 5 смертельных ошибок

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

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

Начать курс

Async/await и Array.map: когда лучшие друзья становятся врагами

Вы когда-нибудь писали что-то вроде const results = await array.map(async (item) => {...}) и получали массив Promise вместо ожидаемых данных? Если да — вы не одиноки. Этот вопрос на Stack Overflow собрал десятки тысяч просмотров, а количество разработчиков, наступивших на эти грабли, исчисляется миллионами.

В этой статье мы разберем 5 самых опасных ошибок при использовании async/await с Array.map в TypeScript. И, что важнее, я покажу, как их избежать. Пристегнитесь — будет больно, но полезно.

Ошибка №1: Забыть про Promise.all (самая популярная)

Самый распространенный баг — думать, что await внутри map магическим образом дождется всех промисов. Спойлер: нет, не дождется.

Проблема

// ❌ Так не работает
const users = [1, 2, 3, 4, 5];
const userData = await users.map(async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
});
// userData — это Promise[], а не UserData[]!
console.log(userData); // [Promise, Promise, Promise, Promise, Promise]

Почему это происходит?

Array.map синхронный. Он не ждет завершения асинхронных колбэков. Каждый вызов async функции возвращает Promise, и map просто собирает их в массив. await перед map ждет только завершения самого map, но не его колбэков.

Решение

// ✅ Правильно: оборачиваем в Promise.all
const users = [1, 2, 3, 4, 5];
const userData = await Promise.all(
  users.map(async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  })
);
// userData — UserData[]! 🎉
console.log(userData);

Ошибка №2: Все промисы выполняются параллельно (а нужно последовательно)

Promise.all запускает все промисы параллельно. Это круто для производительности, но иногда нам нужно выполнять запросы строго один за другим. Например, при работе с API, которое ограничивает количество запросов, или когда каждый следующий запрос зависит от предыдущего.

Проблема

// ❌ Все запросы летят одновременно
const ids = [1, 2, 3, 4, 5];
const results = await Promise.all(
  ids.map(async (id) => {
    // 5 запросов к API одновременно — можно получить rate limit
    return await api.processItem(id);
  })
);

Решение: последовательное выполнение

// ✅ Используем for...of с await
const ids = [1, 2, 3, 4, 5];
const results: ItemData[] = [];

for (const id of ids) {
  const result = await api.processItem(id);
  results.push(result);
}
// results — массив с данными, обработанными по порядку

Альтернативный вариант — Array.reduce с аккумулятором-промисом:

// ✅ Тоже работает, но менее читаемо
const ids = [1, 2, 3, 4, 5];
const results = await ids.reduce(async (accPromise, id) => {
  const acc = await accPromise;
  const result = await api.processItem(id);
  return [...acc, result];
}, Promise.resolve([] as ItemData[]));

Ошибка №3: Неправильная типизация в TypeScript

TypeScript может не сразу понять, что вы делаете с асинхронным map. Особенно если вы используете Promise.all без явного указания типа.

Проблема

// ❌ TypeScript ругается или выводит неправильный тип
const ids = [1, 2, 3];
const results = await Promise.all(
  ids.map(async (id) => {
    const data = await fetchData(id);
    return data.name; // TypeScript может подумать, что это Promise<string>
  })
);
// results имеет тип unknown или Promise<string>[]

Решение: явная аннотация типа

// ✅ Явно указываем тип результата
const ids = [1, 2, 3];
const results: string[] = await Promise.all(
  ids.map(async (id): Promise<string> => {
    const data = await fetchData(id);
    return data.name;
  })
);
// results — string[] ✅

Или используйте Promise.allSettled, если вам нужно обработать как успешные, так и отклоненные промисы:

// ✅ Promise.allSettled с типизацией
const ids = [1, 2, 3];
const results = await Promise.allSettled(
  ids.map(async (id): Promise<string> => {
    const data = await fetchData(id);
    return data.name;
  })
);
// results — PromiseSettledResult<string>[]

Ошибка №4: Игнорировать обработку ошибок

Когда один из промисов в Promise.all падает — падает всё. Без обработки ошибок вы рискуете потерять все данные.

Проблема

// ❌ Если один запрос упадет — потеряем всё
const ids = [1, 2, 3, 4, 5];
try {
  const results = await Promise.all(
    ids.map(async (id) => {
      const data = await riskyApiCall(id); // может упасть
      return data;
    })
  );
} catch (error) {
  // Ловим только первую ошибку, остальные данные потеряны
  console.error('Упал один из запросов:', error);
}

Решение: изолируем ошибки

// ✅ Каждый запрос обрабатываем отдельно
const ids = [1, 2, 3, 4, 5];
const results = await Promise.allSettled(
  ids.map(async (id) => {
    try {
      const data = await riskyApiCall(id);
      return { status: 'fulfilled' as const, value: data };
    } catch (error) {
      return { status: 'rejected' as const, reason: error };
    }
  })
);

// Фильтруем успешные результаты
const successfulResults = results
  .filter(r => r.status === 'fulfilled')
  .map(r => (r as PromiseFulfilledResult<{...}>).value);

console.log(`Успешно: ${successfulResults.length} из ${ids.length}`);

Ошибка №5: Использовать async/await без необходимости

Иногда разработчики автоматически добавляют async к каждому колбэку в map, даже если функция не содержит await. Это создает ненужные промисы и снижает производительность.

Проблема

// ❌ async не нужен, если нет await
const numbers = [1, 2, 3, 4, 5];
const doubled = await Promise.all(
  numbers.map(async (n) => {
    // Нет await, но async есть — лишняя обертка
    return n * 2;
  })
);

Решение

// ✅ Просто map, без async/await и Promise.all
const numbers = [1, 2                

Блоги

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