Gernar
Frontend DeveloperJavaScript, асинхронность

Интервью-вопрос

Чем пользоваться для выполнения запросов нескольких Promise

Короткий ответ: выбирать метод нужно по контракту данных. Promise.all подходит для полного набора результатов, Promise.allSettled для частичного успеха, Promise.any и Promise.race для сценариев с первым исходом, а зависимые операции лучше выполнять последовательно.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Что ответить, если экрану нужны сразу профиль, настройки и права доступа?

Без любого из трех ответов экран нельзя корректно показать.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

На интервью лучше не отвечать одним словом "Promise.all". Надежнее показать, что вы выбираете комбинатор под поведение данных и интерфейса.

Promise.all нужен, когда операции можно стартовать параллельно и нужен полный набор успешных результатов. Promise.allSettled нужен, когда ошибку одного запроса нельзя превращать в полный отказ всего экрана. Promise.race и Promise.any нужны для сценариев с первым завершившимся или первым успешным результатом.

Практический вывод простой. Сначала объясните контракт данных, потом назовите метод. Так ответ звучит не как заученное API, а как инженерное решение для UI.

Как выбрать метод

Как выбрать подход для нескольких Promise

1Вам нужны все результаты, и без любого из них экран бесполезен?
Promise.all и общий обработчик ошибки.
2Можно показать частичные данные и ошибки по отдельным элементам?
Promise.allSettled и разбор статусов fulfilled/rejected.
3Подходит первый успешный ответ из нескольких источников?
Promise.any, с обработкой AggregateError на случай полного провала.
4Важен самый быстрый исход, даже если это ошибка или таймаут?
Promise.race, чаще для timeout или конкурентного сценария.
5Следующий запрос зависит от предыдущего или нужен строгий порядок?
Последовательный цикл с 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. 1Выбрать метод под поведение UI
  2. 2Добавить try/catch или обработку rejected
  3. 3Разобрать частичные ошибки, если они допустимы
  4. 4Ограничить параллельность для больших списков
  5. 5Отменить устаревшие fetch при необходимости
Опасный подход
  1. 1Запустить все запросы без лимита
  2. 2Положиться на Promise.all без catch
  3. 3Считать, что остальные запросы сами отменятся
  4. 4Потерять частичные успешные данные
  5. 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. 1

    Выбирать Promise.all по привычке

    Promise.all хорош, когда без любого результата весь сценарий считается неуспешным. Если на странице можно показать часть карточек, такой выбор ухудшит UX, потому что одна ошибка скроет все успешные ответы. В этом случае лучше использовать Promise.allSettled и показать частичный результат.
  2. 2

    Думать, что Promise.all отменяет остальные операции

    После первой ошибки общий промис отклонится, но уже запущенные fetch продолжат работать. Это может дать лишнюю нагрузку и устаревшие обновления состояния. Если отмена нужна, добавляйте AbortController и cleanup в месте запуска запроса.
  3. 3

    Использовать async внутри forEach

    forEach не ждет await внутри callback и не превращается в последовательную цепочку. Ошибки сложнее поймать, а порядок выполнения будет не тем, который вы ожидаете. Для последовательного сценария используйте for...of с await.
  4. 4

    Не ограничивать параллельность

    Запуск сотен запросов через Promise.all может перегрузить API, браузер и сам интерфейс. Пользователь увидит долгую загрузку, ошибки rate limit или зависания. Для больших списков используйте очередь, пакетную загрузку или лимитер параллельности.
  5. 5

    Путать Promise.race и Promise.any

    Promise.race завершается первым исходом, даже если это ошибка. Promise.any ждет первый успешный результат и падает только если упали все. Неверный выбор может сделать резервный источник бесполезным, потому что ранняя ошибка оборвет сценарий.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание Promise-комбинаторов, ошибок и параллельности.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание