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

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#JavaScript #Асинхронность #База JS

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

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

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

  • Резервные источники — получение данных из первого доступного источника
  • Параллельные запросы — ускорение получения данных через несколько источников
  • Обработка успешных результатов — игнорирование ошибок при наличии хотя бы одного успешного результата

Главная идея:

“Первый успешный результат!”

🎯 Как это работает:

const promise1 = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Ошибка 1')), 500)
);
 
const promise2 = new Promise(resolve => 
    setTimeout(() => resolve('Успех 2'), 200)
);
 
const promise3 = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Ошибка 3')), 300)
);
 
Promise.any([promise1, promise2, promise3])
    .then(result => console.log('Победитель:', result))   // "Победитель: Успех 2"
    .catch(error => console.error('Все провалились:', error));

Полный ответ

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

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

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

const promise1 = new Promise((_, reject) => setTimeout(() => reject(new Error('Ошибка 1')), 1000));
const promise2 = new Promise((_, reject) => setTimeout(() => reject(new Error('Ошибка 2')), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve('Успех 3'), 500));
 
Promise.any([promise1, promise2, promise3])
  .then(result => console.log(result)); // "Успех 3" (первый успешный)

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

1. Резервные источники данных

Один из наиболее распространенных случаев использования Promise.any() — получение данных из первого доступного источника:

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.any(sources);
}
 
// Использование
fetchFromMultipleSources('/api/user-data')
  .then(response => response.json())
  .then(data => console.log('Данные получены из первого доступного источника:', data))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.error('Все источники недоступны:', error.errors);
    } else {
      console.error('Ошибка:', error);
    }
  });

2. Параллельные запросы для ускорения

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

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

3. Проверка доступности сервисов

Можно использовать для проверки доступности первого работающего сервиса:

function checkServiceAvailability() {
  const services = [
    fetch('https://service1.example.com/health'),
    fetch('https://service2.example.com/health'),
    fetch('https://service3.example.com/health')
  ];
  
  return Promise.any(services)
    .then(response => {
      if (response.ok) {
        return 'Сервис доступен';
      } else {
        throw new Error('Сервис не отвечает');
      }
    });
}
 
// Использование
checkServiceAvailability()
  .then(status => console.log(status))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.log('Все сервисы недоступны');
    }
  });

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

Получение данных с таймаутом

function fetchWithFallback(url, fallbackUrls, timeoutMs = 5000) {
  const mainRequest = fetch(url);
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Таймаут')), timeoutMs);
  });
  
  const fallbackRequests = fallbackUrls.map(fbUrl => fetch(fbUrl));
  
  // Сначала ждем основной запрос или таймаут
  return Promise.race([mainRequest, timeout])
    .catch(() => {
      // Если основной запрос не удался, пробуем резервные
      return Promise.any(fallbackRequests);
    });
}
 
// Использование
fetchWithFallback(
  '/api/data',
  ['/api/data-backup1', '/api/data-backup2']
)
  .then(response => response.json())
  .then(data => console.log('Данные:', data))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.error('Все источники недоступны:', error.errors);
    } else {
      console.error('Ошибка:', error.message);
    }
  });

Конкуренция между кэшем и сетью

function fetchWithCachePreference(url) {
  const cacheRequest = getCachedData(url).then(data => {
    if (data) return data;
    throw new Error('Нет данных в кэше');
  });
  
  const networkRequest = fetch(url).then(response => response.json());
  
  return Promise.any([cacheRequest, networkRequest])
    .catch(error => {
      if (error instanceof AggregateError) {
        // Если ни кэш, ни сеть не работают, пробуем еще раз с сетью
        return networkRequest;
      }
      throw error;
    });
}
 
// Использование
fetchWithCachePreference('/api/user-profile')
  .then(data => {
    console.log('Данные получены (первый доступный источник):', data);
    return data;
  })
  .catch(error => {
    if (error instanceof AggregateError) {
      console.error('Все источники недоступны:', error.errors);
    }
  });

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

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

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

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

Если все промисы отклоняются, Promise.any() также отклоняется с AggregateError:

const errorPromise1 = new Promise((_, reject) => setTimeout(() => reject(new Error('Ошибка 1')), 2000));
const errorPromise2 = new Promise((_, reject) => setTimeout(() => reject(new Error('Ошибка 2')), 1000));
 
Promise.any([errorPromise1, errorPromise2])
  .then(result => console.log('Результат:', result))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.error('Все промисы провалились:', error.errors);
    } else {
      console.error('Ошибка:', error.message);
    }
  });

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

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

// ❌ Будет отклонен
Promise.any([])
  .then(result => console.log(result))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.error('Пустой массив промисов');
    }
  });
 
// ✅ Проверка на пустой массив
function safeAny(promises) {
  if (promises.length === 0) {
    return Promise.reject(new Error('Нет промисов для обработки'));
  }
  return Promise.any(promises);
}

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

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

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

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

1. Неправильная обработка AggregateError

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

2. Игнорирование оставшихся промисов

// ❌ Оставшиеся промисы продолжают выполняться
const promises = [slowOperation(), fastOperation()];
Promise.any(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.any(promises)
  .then(response => {
    controller.abort(); // Отменяем другие запросы
    return response.json();
  })
  .then(data => console.log(data));

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

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

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

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

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


Задача для проверки знаний

Задача

Что будет выведено в консоль и почему?

const promise1 = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('Ошибка 1')), 100);
});
 
const promise2 = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('Ошибка 2')), 200);
});
 
const promise3 = new Promise(resolve => {
  setTimeout(() => resolve('Успех 3'), 300);
});
 
Promise.any([promise1, promise2, promise3])
  .then(result => console.log('Результат:', result))
  .catch(error => {
    if (error instanceof AggregateError) {
      console.log('Все провалились:', error.errors.length);
    } else {
      console.log('Другая ошибка:', error.message);
    }
  });
Посмотреть ответ

Ответ: Результат: Успех 3

Объяснение:

  1. Все три промиса запускаются одновременно
  2. promise1 отклоняется через 100мс с “Ошибка 1”
  3. promise2 отклоняется через 200мс с “Ошибка 2”
  4. promise3 разрешается через 300мс со значением “Успех 3”
  5. Promise.any() возвращает первый успешно разрешенный промис, игнорируя отклоненные
  6. Поэтому результатом будет “Успех 3”, и он будет выведен в консоль

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


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