Промисы в JavaScript давно перестали быть новинкой. Но многие разработчики используют лишь базовые возможности: then, catch, finally. А ведь есть ещё куча продвинутых методов, которые делают код чище, надёжнее и эффективнее. В этой статье разберём, как выжать максимум из промисов и не наступить на грабли.

Digital-студия WNDER

Promise.allSettled: когда нужно всё и сразу

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


const promises = [
  fetch('/api/user'),
  fetch('/api/posts'),
  Promise.reject(new Error('Что-то пошло не так'))
];

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Промис ${index} выполнен:`, result.value);
      } else {
        console.error(`Промис ${index} отклонён:`, result.reason);
      }
    });
  });

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

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

Promise.any: первый успешный

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


const mirrors = [
  fetch('https://mirror1.example.com/data'),
  fetch('https://mirror2.example.com/data'),
  fetch('https://mirror3.example.com/data')
];

Promise.any(mirrors)
  .then(response => response.json())
  .then(data => console.log('Данные получены:', data))
  .catch(error => console.error('Все зеркала недоступны:', error));

Обратите внимание: Promise.any не ждёт остальные промисы после первого успеха, но они продолжают выполняться в фоне. Это может быть полезно или вредно в зависимости от задачи.

Обработка ошибок: тонкости и лайфхаки

Ошибки в промисах — больная тема. Многие забывают добавлять catch, что приводит к необработанным отказам. Но есть и более тонкие моменты.

Во-первых, ошибка, брошенная внутри then, перехватывается следующим catch. Но если вы используете несколько then, ошибка пропустит все успешные обработчики и попадёт в ближайший catch.


Promise.resolve('ok')
  .then(result => {
    console.log(result); // 'ok'
    throw new Error('Упс');
  })
  .then(() => console.log('Этот then пропущен'))
  .catch(error => console.error('Поймали:', error.message)); // 'Упс'

Во-вторых, если вы хотите обработать ошибку и продолжить цепочку, возвращайте из catch значение или новый промис.


fetch('/api/data')
  .catch(error => {
    console.warn('Не удалось загрузить, используем кэш');
    return getFromCache();
  })
  .then(data => console.log(data));
Лайфхак: Всегда добавляйте финальный catch в конце цепочки промисов, даже если кажется, что ошибок быть не может. Это спасёт от необработанных rejection'ов.

Отмена промисов и таймауты

Стандартные промисы не поддерживают отмену. Но это не значит, что нельзя реализовать таймаут или отмену запроса. Для этого используют AbortController или гонку с setTimeout.

Пример с таймаутом:


function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Таймаут')), ms)
    )
  ]);
}

withTimeout(fetch('/api/slow'), 5000)
  .then(response => console.log('Успели'))
  .catch(error => console.error(error.message)); // 'Таймаут'

Для отмены fetch используйте AbortController:


const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Запрос отменён');
    }
  });

// Отмена через 2 секунды
setTimeout(() => controller.abort(), 2000);

Продвинутые шаблоны: последовательные и параллельные очереди

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


async function limitedParallel(promises, limit) {
  const results = [];
  const executing = [];
  for (const promise of promises) {
    const p = Promise.resolve().then(() => promise());
    results.push(p);
    if (limit <= promises.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(results);
}

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

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

Итог

Промисы — мощный инструмент, но их истинная сила раскрывается в продвинутых методах. Используйте Promise.allSettled для сбора всех результатов, Promise.any для быстрого успеха, не забывайте про таймауты и отмену. И всегда обрабатывайте ошибки. Тогда ваш асинхронный код станет надёжным и предсказуемым.

Студия WNDER