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