Интервью-вопрос
Чем пользоваться для выполнения запросов нескольких Promise
Короткий ответ: выбирать метод нужно по контракту данных. Promise.all подходит для полного набора результатов, Promise.allSettled для частичного успеха, Promise.any и Promise.race для сценариев с первым исходом, а зависимые операции лучше выполнять последовательно.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 50 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
На интервью лучше не отвечать одним словом "Promise.all". Надежнее показать, что вы выбираете комбинатор под поведение данных и интерфейса.
Promise.all нужен, когда операции можно стартовать параллельно и нужен полный набор успешных результатов. Promise.allSettled нужен, когда ошибку одного запроса нельзя превращать в полный отказ всего экрана. Promise.race и Promise.any нужны для сценариев с первым завершившимся или первым успешным результатом.
Практический вывод простой. Сначала объясните контракт данных, потом назовите метод. Так ответ звучит не как заученное API, а как инженерное решение для UI.
Как выбрать метод
Как выбрать подход для нескольких Promise
Promise.all и общий обработчик ошибки.Promise.allSettled и разбор статусов fulfilled/rejected.Promise.any, с обработкой AggregateError на случай полного провала.Promise.race, чаще для timeout или конкурентного сценария.Последовательный цикл с await.Хорошая формулировка ответа может звучать так:
Если все запросы независимы и экрану нужны все данные, я запущу их параллельно через Promise.all. Если часть данных можно показать независимо, выберу Promise.allSettled и отдельно обработаю успешные и упавшие результаты. Если запросы зависят друг от друга, буду выполнять их последовательно.
Такая формулировка закрывает главный риск вопроса. Не все наборы Promise нужно выполнять одинаково. Иногда параллельность ускоряет экран, иногда ломает порядок, а иногда скрывает частично успешные данные.
Пример для Promise.all и обработки ошибки
Для независимых запросов, без которых экран нельзя показать, подойдет Promise.all.
async function loadPageData() {
try {
const [profileResponse, settingsResponse, permissionsResponse] = await Promise.all([
fetch("/api/profile"),
fetch("/api/settings"),
fetch("/api/permissions"),
]);
if (!profileResponse.ok || !settingsResponse.ok || !permissionsResponse.ok) {
throw new Error("Failed to load page data");
}
const [profile, settings, permissions] = await Promise.all([
profileResponse.json(),
settingsResponse.json(),
permissionsResponse.json(),
]);
return { profile, settings, permissions };
} catch (error) {
// Показать общий error state или retry, потому что экран зависит от всех данных.
throw error;
}
}Важно помнить, что fetch сам по себе не считает HTTP 404 или 500 rejected. Он обычно успешно возвращает Response, поэтому статус нужно проверить отдельно через response.ok. Иначе можно получить успешный промис с ошибочным HTTP-ответом и сломать дальнейший парсинг или UI.
Когда нужен allSettled
Если данные независимы, не всегда стоит валить весь экран из-за одной ошибки. Например, рекомендации могут не загрузиться, но профиль пользователя и список заказов все равно можно показать.
async function requestJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${url}`);
}
return response.json();
}
async function loadDashboard() {
const results = await Promise.allSettled([
requestJson("/api/profile"),
requestJson("/api/orders"),
requestJson("/api/recommendations"),
]);
return results.map((result) => {
if (result.status === "fulfilled") {
return { status: "success", data: result.value };
}
return { status: "error", error: result.reason };
});
}Здесь вы явно сохраняете информацию по каждому запросу. Проверка response.ok важна и в этом варианте тоже. Без нее HTTP 500 может попасть в UI как успешный результат. Это полезно для UI с частичным успехом, но вам все равно нужно продумать состояние каждого блока: загрузка, данные, ошибка и retry.
Последовательное выполнение
Если следующий запрос зависит от предыдущего, параллельный запуск через Promise.all будет ошибкой. Например, нельзя оплатить заказ до того, как сервер вернул его orderId.
Плохой вариант:
// Плохо: payment не знает orderId в момент запуска.
await Promise.all([
createOrder(cart),
payForOrder(orderId),
]);Безопаснее выполнить шаги явно:
const order = await createOrder(cart);
const payment = await payForOrder(order.id);
const status = await loadOrderStatus(order.id);На интервью стоит сказать, что последовательность медленнее параллельного запуска, но она нужна, когда есть зависимость данных, требование порядка или побочный эффект на сервере.
Практические риски во фронтенде
- 1Выбрать метод под поведение UI
- 2Добавить try/catch или обработку rejected
- 3Разобрать частичные ошибки, если они допустимы
- 4Ограничить параллельность для больших списков
- 5Отменить устаревшие fetch при необходимости
- 1Запустить все запросы без лимита
- 2Положиться на Promise.all без catch
- 3Считать, что остальные запросы сами отменятся
- 4Потерять частичные успешные данные
- 5Обновить UI устаревшим ответом
Главный риск не в синтаксисе, а в поведении интерфейса. Если запустить слишком много запросов одновременно, можно получить rate limit, долгую очередь в браузере, резкие скачки loading state и тяжелую обработку ответов на главном потоке.
Если компонент может размонтироваться или пользователь быстро меняет фильтры, подумайте об отмене устаревших запросов. Для fetch это делают через AbortController. Иначе старый ответ может прийти позже нового и перезаписать актуальные данные.
useEffect(() => {
const controller = new AbortController();
Promise.all([
fetch(`/api/products?query=${query}`, { signal: controller.signal }),
fetch(`/api/facets?query=${query}`, { signal: controller.signal }),
])
.then(([productsResponse, facetsResponse]) => {
if (!productsResponse.ok || !facetsResponse.ok) {
throw new Error("Failed to load search data");
}
return Promise.all([productsResponse.json(), facetsResponse.json()]);
})
.then(([products, facets]) => {
setProducts(products);
setFacets(facets);
})
.catch((error) => {
if (error.name !== "AbortError") {
setError(error);
}
});
return () => controller.abort();
}, [query]);Без cleanup старый запрос может обновить состояние после нового поиска. В React это дает race condition. Пользователь видит результаты не для текущего фильтра. Отмена не заменяет обработку ошибок, но убирает лишнюю сетевую работу и защищает UI от устаревшего ответа.
Частые ошибки
Где обычно ошибаются
Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.
- 1
Выбирать Promise.all по привычке
Promise.allхорош, когда без любого результата весь сценарий считается неуспешным. Если на странице можно показать часть карточек, такой выбор ухудшит UX, потому что одна ошибка скроет все успешные ответы. В этом случае лучше использоватьPromise.allSettledи показать частичный результат. - 2
Думать, что Promise.all отменяет остальные операции
После первой ошибки общий промис отклонится, но уже запущенныеfetchпродолжат работать. Это может дать лишнюю нагрузку и устаревшие обновления состояния. Если отмена нужна, добавляйтеAbortControllerи cleanup в месте запуска запроса. - 3
Использовать async внутри forEach
forEachне ждетawaitвнутри callback и не превращается в последовательную цепочку. Ошибки сложнее поймать, а порядок выполнения будет не тем, который вы ожидаете. Для последовательного сценария используйтеfor...ofсawait. - 4
Не ограничивать параллельность
Запуск сотен запросов черезPromise.allможет перегрузить API, браузер и сам интерфейс. Пользователь увидит долгую загрузку, ошибки rate limit или зависания. Для больших списков используйте очередь, пакетную загрузку или лимитер параллельности. - 5
Путать Promise.race и Promise.any
Promise.raceзавершается первым исходом, даже если это ошибка.Promise.anyждет первый успешный результат и падает только если упали все. Неверный выбор может сделать резервный источник бесполезным, потому что ранняя ошибка оборвет сценарий.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание Promise-комбинаторов, ошибок и параллельности.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Что будет, если в Promise.all один Promise выдаст ошибку 😎
Promise.all отклонится с причиной первого отклоненного Promise, но не отменит остальные операции. Разберитесь, как ответить на интервью и когда выбрать Promise.all, Promise.allSettled или локальную обработку ошибок.
Что использовали раньше до появления Promise в JavaScript 😎
До Promise асинхронность чаще строили на callback-функциях, событиях, Deferred-объектах и библиотеках для контроля потока. На странице разбираем, что сказать на интервью и какие ограничения этих подходов важно назвать.