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

Digital-студия WNDER

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

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

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

const userPromise = fetch('/api/user/1');
const ordersPromise = fetch('/api/orders?user=1');
const settingsPromise = fetch('/api/settings/1');

Promise.all([userPromise, ordersPromise, settingsPromise])
  .then(([user, orders, settings]) => {
    console.log('Пользователь:', user);
    console.log('Заказы:', orders);
    console.log('Настройки:', settings);
  })
  .catch(error => {
    console.error('Что-то пошло не так:', error);
  });

Если хотя бы один промис отклонится, весь Promise.all сразу перейдёт в состояние rejected. Это как если бы суп пригорел — всё блюдо испорчено.

Правило: Используйте Promise.all, когда все результаты важны и нужны вместе. Если падение одного не критично — смотрите на Promise.allSettled.

Promise.race и Promise.any: кто быстрее

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

Пример: запрашиваем данные с двух серверов и берём тот, который ответит быстрее.

const server1 = fetch('https://api1.example.com/data');
const server2 = fetch('https://api2.example.com/data');

Promise.race([server1, server2])
  .then(response => response.json())
  .then(data => console.log('Победитель:', data))
  .catch(error => console.error('Все упали:', error));

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

const p1 = Promise.reject('Ошибка 1');
const p2 = new Promise(resolve => setTimeout(resolve, 100, 'Успех!'));
const p3 = Promise.reject('Ошибка 2');

Promise.any([p1, p2, p3])
  .then(result => console.log(result)) // 'Успех!'
  .catch(errors => console.error(errors.errors));

Promise.allSettled: когда важно знать обо всех

Бывает, что нужно дождаться выполнения всех промисов, но неважно, успешны они или нет. Например, вы отправляете несколько запросов на сервер и хотите собрать статистику: сколько успешных, сколько с ошибками. Promise.allSettled возвращает массив объектов с полями status и value (или reason).

const promises = [
  fetch('/api/1'),
  fetch('/api/2'),
  Promise.reject('Сломалось')
];

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

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

Цепочки промисов и обработка ошибок

Цепочки .then() — это классика, но есть нюансы. Если внутри .then() вы бросаете ошибку, она перехватывается следующим .catch(). Если не бросаете — цепочка продолжается. Важно возвращать промисы из .then(), чтобы сохранить последовательность.

function getUser(id) {
  return fetch(`/api/user/${id}`).then(res => res.json());
}

function getOrders(userId) {
  return fetch(`/api/orders?user=${userId}`).then(res => res.json());
}

getUser(1)
  .then(user => {
    console.log('Пользователь:', user);
    return getOrders(user.id); // возвращаем промис
  })
  .then(orders => {
    console.log('Заказы:', orders);
  })
  .catch(error => {
    console.error('Ошибка в цепочке:', error);
  });

Если забыть return, следующий .then() получит undefined и цепочка сломается.

Лайфхак: Всегда добавляйте .catch() в конец цепочки, иначе необработанная ошибка может уронить приложение.

Асинхронные функции: async/await поверх промисов

async/await — это синтаксический сахар над промисами. Он делает код похожим на синхронный, но под капотом всё те же промисы. Главное правило: await можно использовать только внутри async-функции.

async function loadUserData(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    return { user, orders };
  } catch (error) {
    console.error('Ошибка загрузки:', error);
    throw error; // пробрасываем дальше
  }
}

loadUserData(1).then(data => console.log(data));

Обратите внимание: await останавливает выполнение функции до разрешения промиса, но не блокирует весь поток JavaScript. Другие задачи продолжают работать.

Если нужно выполнить несколько промисов параллельно, используйте Promise.all вместе с await:

async function loadAll() {
  const [user, orders, settings] = await Promise.all([
    fetch('/api/user/1').then(r => r.json()),
    fetch('/api/orders?user=1').then(r => r.json()),
    fetch('/api/settings/1').then(r => r.json())
  ]);
  return { user, orders, settings };
}

Сравнение методов: таблица

Чтобы не запутаться, вот шпаргалка по методам:

Метод Когда завершается Результат Обработка ошибок
Promise.all Все успешны Массив значений Первый reject
Promise.race Первый завершённый Значение или ошибка Первый reject
Promise.any Первый успешный Значение AggregateError, если все reject
Promise.allSettled Все завершены Массив объектов статуса Нет reject

Практические советы и лайфхаки

  • Не забывайте про return. В цепочках .then() всегда возвращайте промис или значение, иначе следующий шаг получит undefined.
  • Используйте async/await для читаемости. Если логика сложная, async/await с try/catch выглядит понятнее, чем длинные цепочки.
  • Promise.all с ограничением параллелизма. Если промисов слишком много, можно разбить их на группы, чтобы не перегружать сервер.
  • Обрабатывайте ошибки в каждом промисе. Если один промис в Promise.all упадёт, остальные всё равно выполнятся, но результат потеряется. Иногда лучше обернуть каждый промис в .catch(), чтобы получить частичный результат.
Совет: Для отладки промисов используйте console.log внутри .then() и .catch(), но не забывайте удалять их в продакшене.

Ещё один приём — Promise.resolve() и Promise.reject() для создания уже разрешённых или отклонённых промисов. Это полезно в тестах или когда нужно вернуть промис из синхронной функции.

function maybeAsync(value) {
  if (value > 0) {
    return Promise.resolve(value);
  } else {
    return Promise.reject('Отрицательное значение');
  }
}

maybeAsync(5).then(console.log); // 5
maybeAsync(-1).catch(console.error); // 'Отрицательное значение'

Что в итоге

Промисы — мощный инструмент, но их продвинутые методы требуют понимания. Promise.all для параллельных задач, Promise.race для гонок, Promise.any для первого успеха, Promise.allSettled для сбора всех результатов. Не забывайте про async/await для читаемости и всегда обрабатывайте ошибки. Практикуйтесь, и асинхронный код станет вашим вторым языком.

Студия WNDER