Для чего нужен Promise.race()?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#JavaScript #Асинхронность #База JS

Краткий ответ

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

Основные применения:

  • Таймауты — ограничение времени выполнения асинхронных операций
  • Гонки данных — получение данных из самого быстрого источника
  • Отмена операций — прекращение выполнения при первом результате

Основная идея:

“Кто первый ответит — того и тапки!” 🏁

🏎️ Как работает:

const promise1 = new Promise(resolve => 
    setTimeout(() => resolve('Победитель 1'), 500)
);
 
const promise2 = new Promise(resolve => 
    setTimeout(() => resolve('Победитель 2'), 200)
);
 
const promise3 = new Promise((resolve, reject) => 
    setTimeout(() => reject('Ошибка!'), 300)
);
 
Promise.race([promise1, promise2, promise3])
    .then(result => console.log('Победил:', result))   // "Победил: Победитель 2"
    .catch(error => console.error('Проиграл:', error));

Полный ответ

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

Как работает Promise.race()

Promise.race() принимает итерируемый объект (обычно массив) промисов и возвращает новый промис:

const promise1 = new Promise(resolve => setTimeout(() => resolve(1), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve(3), 500));
 
Promise.race([promise1, promise2, promise3])
  .then(result => console.log(result)); // 3 (самый быстрый)

Основные сценарии использования

1. Реализация таймаутов

Одно из самых распространенных применений Promise.race() — добавление таймаута к асинхронным операциям:

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Таймаут')), ms);
  });
  
  return Promise.race([promise, timeout]);
}
 
// Использование
withTimeout(fetch('/api/data'), 5000)
  .then(response => console.log('Данные получены'))
  .catch(error => {
    if (error.message === 'Таймаут') {
      console.log('Запрос превысил время ожидания');
    } else {
      console.error('Ошибка запроса:', error);
    }
  });

2. Получение данных из самого быстрого источника

Полезно при наличии нескольких источников одних и тех же данных:

function fetchFromMultipleSources(url) {
  const sources = [
    fetch(`https://fast-cdn.com${url}`),
    fetch(`https://backup-server.com${url}`),
    fetch(`https://mirror-site.com${url}`)
  ];
  
  return Promise.race(sources);
}
 
// Использование
fetchFromMultipleSources('/api/user-data')
  .then(response => response.json())
  .then(data => console.log('Данные получены из самого быстрого источника:', data));

3. Отмена длительных операций

Можно использовать для отмены операций при первом сигнале:

function cancellableOperation(operationPromise, cancelSignal) {
  return Promise.race([
    operationPromise,
    cancelSignal.then(() => Promise.reject(new Error('Операция отменена')))
  ]);
}
 
// Использование
const longOperation = fetch('/api/long-process');
const cancelButton = document.getElementById('cancel');
 
const cancelSignal = new Promise((resolve) => {
  cancelButton.addEventListener('click', () => resolve());
});
 
cancellableOperation(longOperation, cancelSignal)
  .then(result => console.log('Операция завершена:', result))
  .catch(error => {
    if (error.message === 'Операция отменена') {
      console.log('Операция была отменена пользователем');
    }
  });

Практические примеры

Таймаут для fetch запроса

function fetchWithTimeout(url, timeoutMs = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Превышено время ожидания')), timeoutMs);
  });
  
  return Promise.race([fetchPromise, timeoutPromise]);
}
 
// Использование
fetchWithTimeout('/api/data', 3000)
  .then(response => {
    if (!response.ok) throw new Error('Ошибка сети');
    return response.json();
  })
  .then(data => console.log('Данные:', data))
  .catch(error => {
    if (error.message.includes('Превышено')) {
      console.error('Запрос занял слишком много времени');
    } else {
      console.error('Ошибка:', error.message);
    }
  });

Гонка между кэшем и сетью

function fetchWithCacheFallback(url) {
  const cachePromise = getCachedData(url);
  const networkPromise = fetch(url).then(response => response.json());
  
  return Promise.race([cachePromise, networkPromise])
    .catch(() => {
      // Если первый промис завершился с ошибкой, ждем второй
      return Promise.race([networkPromise, cachePromise]);
    });
}
 
// Использование
fetchWithCacheFallback('/api/user-profile')
  .then(data => {
    console.log('Данные получены (быстрее всего):', data);
    return data;
  })
  .catch(error => console.error('Все источники недоступны:', error));

Конкуренция между несколькими API

function fetchFromFastestAPI(endpoint) {
  const apis = [
    fetch(`https://api1.example.com${endpoint}`),
    fetch(`https://api2.example.com${endpoint}`),
    fetch(`https://api3.example.com${endpoint}`)
  ];
  
  return Promise.race(apis)
    .then(response => {
      if (!response.ok) throw new Error('Ошибка API');
      return response.json();
    });
}
 
// Использование
fetchFromFastestAPI('/users/123')
  .then(userData => {
    console.log('Данные пользователя из самого быстрого API:', userData);
  })
  .catch(error => console.error('Все API недоступны:', error));

Особенности и поведение

1. Обработка ошибок

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

const successPromise = new Promise(resolve => setTimeout(() => resolve('успех'), 2000));
const errorPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('ошибка')), 1000));
 
Promise.race([successPromise, errorPromise])
  .then(result => console.log('Результат:', result))
  .catch(error => console.error('Ошибка:', error.message)); // 'ошибка'

2. Пустой массив

Если передать пустой массив, Promise.race() повиснет в состоянии ожидания:

// ❌ Никогда не разрешится
Promise.race([])
  .then(result => console.log(result));
 
// ✅ Проверка на пустой массив
function safeRace(promises) {
  if (promises.length === 0) {
    return Promise.resolve();
  }
  return Promise.race(promises);
}

3. Непромисные значения

Promise.race() преобразует непромисные значения в разрешенные промисы:

Promise.race([42, Promise.resolve('строка'), new Promise(resolve => setTimeout(() => resolve(true), 1000))])
  .then(result => console.log(result)); // 42 (число мгновенно преобразуется в разрешенный промис)

Распространенные ошибки и их решения

1. Неправильная обработка всех возможных исходов

// ❌ Может пропустить ошибки
Promise.race([fetch('/api/data1'), fetch('/api/data2')])
  .then(data => console.log(data));
 
// ✅ Правильная обработка
Promise.race([fetch('/api/data1'), fetch('/api/data2')])
  .then(response => {
    if (!response.ok) throw new Error('Ошибка сети');
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error('Ошибка в гонке:', error));

2. Игнорирование остальных промисов

// ❌ Остальные промисы продолжают выполняться
const promises = [slowOperation(), fastOperation()];
Promise.race(promises)
  .then(result => {
    console.log('Быстрый результат:', result);
    // slowOperation() все еще выполняется в фоне
  });
 
// ✅ Отмена остальных операций (с использованием AbortController)
const controller = new AbortController();
const signal = controller.signal;
 
const promises = [
  fetch('/api/slow', { signal }),
  fetch('/api/fast', { signal })
];
 
Promise.race(promises)
  .then(response => {
    controller.abort(); // Отменяем остальные запросы
    return response.json();
  })
  .then(data => console.log(data));

Лучшие практики

  1. Всегда обрабатывайте ошибки — используйте .catch() для обработки отклонений
  2. Используйте для таймаутов — эффективный способ ограничить время выполнения
  3. Применяйте для резервных источников — получайте данные из самого быстрого источника
  4. Отменяйте ненужные операции — экономьте ресурсы при первом результате
  5. Проверяйте пустые массивы — избегайте зависания Promise.race()

Ключевые преимущества Promise.race()

  1. Производительность — мгновенная реакция на первый результат
  2. Гибкость — поддержка различных сценариев гонок
  3. Простота использования — интуитивный API
  4. Совместимость — работает со всеми типами промисов
  5. Экономия ресурсов — возможность отмены остальных операций

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


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪