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

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

Что будет, если в Promise.all один Promise выдаст ошибку

Promise.all отклонится с ошибкой первого отклоненного Promise. Остальные операции не отменяются автоматически. Во frontend-коде важно заранее выбрать стратегию обработки ошибок.

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

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

🐰0
🥚0

Мини-квиз

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

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

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

Что ответить про первую ошибку в Promise.all?

Вы объясняете поведение, когда один из нескольких Promise перешел в rejected.

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

Разбор

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

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

Базовая идея

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

Главное слово для интервью: fail fast. Promise.all быстро сообщает об ошибке и не ждет, пока вы получите полный отчет по каждому элементу. Но это не значит, что он физически остановил остальные задачи.

В успешном сценарии массив значений идет в порядке входного массива. Скорость завершения отдельных Promise на порядок значений не влияет.

Пример с первой ошибкой

В этом примере ошибка приходит раньше, чем завершается медленный Promise. Общий catch сработает по первой ошибке.

const slowUser = new Promise((resolve) => {
  setTimeout(() => resolve("user"), 1000);
});

const fastError = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("profile failed")), 100);
});

const settings = Promise.resolve("settings");

Promise.all([slowUser, fastError, settings])
  .then((values) => {
    console.log(values);
  })
  .catch((error) => {
    console.error(error.message); // "profile failed"
  });

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

Что важно во frontend-коде

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

Плохой пример: вы отправили несколько независимых изменений через Promise.all, одно упало, и вы показываете пользователю "ничего не сохранилось". Но часть запросов могла успешно изменить данные на сервере. Так появляется рассинхронизация UI и backend.

Безопаснее заранее решить, какой контракт нужен. Если операция атомарная с точки зрения пользователя, лучше иметь серверный endpoint, который применяет изменения транзакционно. Если операции независимые, показывайте результат по каждой операции через Promise.allSettled или свой формат результата.

Как выбрать подход

1Нужен результат только если все операции успешны?
Используйте Promise.all и обрабатывайте общий catch.
2Нужно показать частичные данные и ошибки по каждому элементу?
Используйте Promise.allSettled.
3Одна ошибка некритична и есть fallback?
Обработайте catch у конкретного Promise и верните явный fallback.
4После первой ошибки надо остановить сетевые запросы?
Добавьте явную отмену, например AbortController для fetch.

Как обработать некритичную ошибку

Иногда падение одного Promise не должно ломать всю страницу. Например, основной профиль нужен обязательно, а блок рекомендаций можно заменить пустым списком. В таком случае обработайте ошибку на уровне конкретного Promise.

const profilePromise = fetchProfile();

const recommendationsPromise = fetchRecommendations().catch((error) => {
  console.warn("Recommendations are unavailable", error);
  return [];
});

const [profile, recommendations] = await Promise.all([
  profilePromise,
  recommendationsPromise,
]);

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

Когда нужен Promise.allSettled

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

const results = await Promise.allSettled([
  fetchHeader(),
  fetchSidebar(),
  fetchFeed(),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    renderBlock(result.value);
  } else {
    renderBlockError(result.reason);
  }
}

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

Отмена и side effects

Promise.all не отменяет операции. Если вам надо остановить сетевые запросы после ошибки или при уходе со страницы, используйте механизм конкретного API. Для fetch это обычно AbortController.

const controller = new AbortController();

try {
  await Promise.all([
    fetch("/api/user", { signal: controller.signal }),
    fetch("/api/settings", { signal: controller.signal }),
  ]);
} catch (error) {
  controller.abort();
  showError(error);
}

Такой код показывает идею, но отмена в catch не откатывает уже выполненные действия на сервере. Она только просит отменить операции, которые еще можно остановить. В реальном компоненте еще нужен cleanup при размонтировании. Иначе поздний ответ может прийти, когда экран уже закрыт. Пользователь увидит устаревшее состояние или лишнюю ошибку.

useEffect(() => {
  const controller = new AbortController();
  let active = true;

  async function loadJson(url) {
    const response = await fetch(url, { signal: controller.signal });

    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }

    return response.json();
  }

  async function load() {
    try {
      const [user, settings] = await Promise.all([
        loadJson("/api/user"),
        loadJson("/api/settings"),
      ]);

      if (!active) return;
      setData({ user, settings });
    } catch (error) {
      if (!active || error.name === "AbortError") return;
      setError(error);
    }
  }

  load();

  return () => {
    active = false;
    controller.abort();
  };
}, []);

Безопасная идея здесь не в самом Promise.all. Она в контракте вокруг него: при уходе со страницы запросы отменяются, а поздний результат не обновляет состояние.

Безопасный ответ в UI
  1. 1Запускаете независимые операции параллельно
  2. 2Ловите общую ошибку у Promise.all
  3. 3Показываете понятное состояние ошибки
  4. 4При необходимости явно отменяете лишние запросы
Опасное ожидание
  1. 1Считаете, что Promise.all отменит остальные операции
  2. 2Не учитываете side effects от продолжающихся запросов
  3. 3Скрываете причину ошибки общим сообщением
  4. 4Теряете частичные результаты без allSettled

Практический вывод

Хороший короткий ответ на интервью может звучать так:

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

Такой ответ показывает не только знание метода. Он показывает понимание риска для интерфейса: частичные side effects, потерянные результаты и неверное состояние загрузки.

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Думать, что остальные Promise отменяются

    Promise.all отклоняет только свой результирующий Promise. Запросы, таймеры и записи, которые уже стартовали, продолжают жить. Если это важно для UI, добавьте явную отмену или защиту от позднего обновления состояния.
  2. 2

    Путать первую ошибку по времени с первым элементом массива

    Ошибка приходит от того Promise, который первым перешел в состояние rejected. Его позиция во входном массиве не обязана быть первой. На интервью скажите именно "первый отклоненный по времени".
  3. 3

    Ждать частичные результаты от Promise.all

    При отклонении вы не получите массив успешных значений через then. Если интерфейсу нужны данные по каждому виджету, используйте Promise.allSettled или возвращайте явные объекты результата для каждого запроса.
  4. 4

    Скрывать критичные ошибки локальным catch

    promise.catch(() => null) делает ошибку похожей на нормальное значение. Так легко получить тихо сломанный UI. Безопаснее вернуть объект вида { ok: false, error } или обработать только ожидаемую ошибку.
  5. 5

    Использовать Promise.all для зависимых шагов

    Если второй запрос требует id из первого, параллельный запуск даст гонки или неверные параметры. Для зависимых операций используйте последовательный await. Promise.all оставляйте для независимых задач.
  6. 6

    Не защищать состояние компонента от позднего ответа

    После первой ошибки общий catch уже мог показать ошибку, но остальные запросы еще завершаются. В React это может обновить устаревший экран. Используйте AbortController, cleanup в useEffect или проверку актуального запроса перед setState.

Follow-up

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

Короткие ответы на вопросы, которые помогают проверить понимание Promise.all, ошибок и параллельных операций.

Живые ответы

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

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

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

Содержание