Learn JavaScript

Promise.race() - ожидание первого завершившегося промиса в JavaScript

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

Синтаксис

Базовая запись выглядит так:

Promise.race(iterable)

Где:

  • iterable - итерируемый набор значений, чаще всего массив

Обычно в Promise.race() передают массив промисов, но внутри могут быть и обычные значения.

Что возвращает Promise.race()

Метод всегда возвращает объект Promise.

Итоговый промис завершится тогда, когда завершится первый элемент из переданного набора.

const promise = Promise.race([
  new Promise((resolve) => setTimeout(() => resolve("slow"), 50)),
  new Promise((resolve) => setTimeout(() => resolve("fast"), 10)),
]);

console.log(promise instanceof Promise); // true

promise.then((value) => {
  console.log(value); // "fast"
});

Здесь итоговый промис получает значение "fast", потому что второй элемент завершился раньше.

Простой пример

const first = new Promise((resolve) => {
  setTimeout(() => resolve("Первый ответ"), 100);
});

const second = new Promise((resolve) => {
  setTimeout(() => resolve("Второй ответ"), 50);
});

Promise.race([first, second]).then((value) => {
  console.log(value); // "Второй ответ"
});

Это удобно, когда нужен самый быстрый результат, а не все результаты сразу.

Promise.race() завершается по первому результату

Это главное поведение метода.

Promise.race() не ждёт, пока завершатся все элементы. Он заканчивает работу сразу, как только первый элемент перейдёт в состояние fulfilled или rejected.

const slow = new Promise((resolve) => {
  setTimeout(() => resolve("slow"), 100);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("fast"), 10);
});

Promise.race([slow, fast]).then((value) => {
  console.log(value); // "fast"
});

Здесь неважно, что slow тоже когда-то завершится. Победителем становится тот, кто завершился первым.

Первый успешный результат делает итоговый промис успешным

Если первым завершился промис со значением, итоговый Promise.race() станет fulfilled.

const fastSuccess = new Promise((resolve) => {
  setTimeout(() => resolve("ok"), 10);
});

const slowSuccess = new Promise((resolve) => {
  setTimeout(() => resolve("later"), 50);
});

Promise.race([fastSuccess, slowSuccess]).then((value) => {
  console.log(value); // "ok"
});

Результатом будет одно значение, а не массив значений.

Первая ошибка делает итоговый промис отклонённым

Это важный момент, который часто путают с Promise.any().

Если раньше всех завершится промис с ошибкой, итоговый Promise.race() сразу станет rejected.

const slowSuccess = new Promise((resolve) => {
  setTimeout(() => resolve("done"), 50);
});

const fastReject = new Promise((_, reject) => {
  setTimeout(() => reject(new Error("boom")), 10);
});

Promise.race([slowSuccess, fastReject])
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error.message); // "boom"
  });

То есть Promise.race() реагирует на первое завершение вообще, а не только на первый успех.

Обычные значения тоже участвуют в гонке

Это ещё один полезный нюанс.

В Promise.race() можно передавать не только промисы, но и обычные значения. Они рассматриваются как уже успешно готовые.

console.log("start");

Promise.race([42]).then((value) => {
  console.log(value); // 42
});

console.log("end");

Результат будет таким:

start
end
42

Даже если внутри уже готовое значение, обработчик then() всё равно выполнится асинхронно.

Пустой массив оставляет промис в состоянии pending

Это очень важная особенность Promise.race().

Если передать пустой массив, у метода просто не будет участника, который мог бы завершиться первым. Поэтому итоговый промис так и останется в состоянии pending.

const promise = Promise.race([]);

promise.then(() => {
  console.log("done");
});

setTimeout(() => {
  console.log("still pending"); // "still pending"
}, 100);

Здесь then() не выполнится, потому что гонка так и не начнётся по-настоящему.

Остальные операции не отменяются автоматически

Это один из самых важных практических нюансов.

Promise.race() выбирает первый завершившийся результат, но не останавливает остальные уже запущенные операции.

const slow = new Promise((resolve) => {
  setTimeout(() => {
    console.log("slow done");
    resolve("slow");
  }, 50);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("fast"), 10);
});

Promise.race([slow, fast]).then((value) => {
  console.log("winner:", value); // "winner: fast"
});

Сначала вы получите результат победителя, но код внутри slow всё равно продолжит выполняться.

Это особенно важно помнить при запросах, таймерах и других побочных эффектах.

Promise.race() часто используют для таймаута

Это один из самых популярных практических сценариев.

function fetchWithTimeout(url, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error("Время ожидания истекло"));
    }, timeoutMs);
  });

  return Promise.race([fetch(url), timeoutPromise]);
}

fetchWithTimeout("/api/data", 3000)
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error.message);
  });

Такой код перестаёт ждать слишком долгий результат и переходит в catch(), если раньше сработал таймер.

Но есть важный нюанс: сам fetch() при этом не отменяется автоматически. Если нужна именно отмена запроса, обычно используют AbortController.

Чем Promise.race() отличается от Promise.any()

Это сравнение полезно запомнить.

  • Promise.race() завершается на первом результате вообще: и на успехе, и на ошибке
  • Promise.any() завершается на первом успешном результате и не падает на первой ошибке
Promise.race([
  Promise.reject(new Error("boom")),
  Promise.resolve("ok"),
]).catch((error) => {
  console.log("race:", error.message); // "race: boom"
});

Если нужен именно первый успешный ответ, Promise.race() подходит не всегда.

Когда это полезно

Promise.race() особенно удобен, когда:

  • нужен самый быстрый ответ из нескольких источников
  • нужно ограничить ожидание таймаутом
  • важен первый доступный результат, а не полный набор результатов
  • нужно быстрее продолжить выполнение, как только определится победитель

Пример:

Promise.race([
  fetch("/api/mirror-1").then((response) => response.json()),
  fetch("/api/mirror-2").then((response) => response.json()),
])
  .then((data) => {
    console.log("Самый быстрый источник:", data);
  })
  .catch((error) => {
    console.log("Ошибка:", error);
  });

Такой подход удобен, если несколько источников могут отдать одинаковые данные, и достаточно первого ответа.

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

Думать, что Promise.race() ждёт первый успех

const slowSuccess = new Promise((resolve) => {
  setTimeout(() => resolve("ok"), 50);
});

const fastReject = new Promise((_, reject) => {
  setTimeout(() => reject(new Error("boom")), 10);
});

Promise.race([slowSuccess, fastReject]).catch((error) => {
  console.log(error.message); // "boom"
});

Метод реагирует на первое завершение, а не на первый успешный результат.

Ожидать массив результатов

Promise.race([
  Promise.resolve("A"),
  Promise.resolve("B"),
]).then((value) => {
  console.log(value); // "A"
});

Promise.race() возвращает одно итоговое значение или одну итоговую ошибку, а не массив.

Думать, что проигравшие операции автоматически останавливаются

const slow = new Promise((resolve) => {
  setTimeout(() => {
    console.log("slow done");
    resolve("slow");
  }, 50);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve("fast"), 10);
});

Promise.race([slow, fast]).then((value) => {
  console.log(value); // "fast"
});

После завершения гонки остальные операции могут продолжать выполняться.

Передавать пустой массив и ждать then()

Promise.race([]).then(() => {
  console.log("done");
});

Такой обработчик не выполнится, потому что итоговый промис останется в состоянии pending.

Передавать неитерируемое значение

Promise.race(123).catch((error) => {
  console.log(error.name); // "TypeError"
});

Метод ожидает массив или другой итерируемый объект.

Быстрая памятка

  • Promise.race() вызывается у Promise
  • метод всегда возвращает объект Promise
  • итог зависит от первого завершившегося элемента
  • первый результат может быть и успешным значением, и ошибкой
  • результатом будет одно значение или одна ошибка, а не массив
  • обычные значения внутри массива допустимы
  • пустой массив оставляет итоговый промис в состоянии pending
  • Promise.race() не отменяет остальные уже запущенные операции автоматически

Коротко

Promise.race() в JavaScript нужен, когда важен первый завершившийся результат из нескольких операций. Главное, что стоит запомнить: метод завершается на первом fulfilled или rejected, возвращает один итоговый результат, а не массив, умеет работать с обычными значениями, оставляет пустую гонку в состоянии pending и особенно полезен для таймаутов и сценариев, где достаточно самого быстрого ответа.