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

Digital-студия WNDER

Цепочки промисов: как не заблудиться в then

Цепочки промисов — это как конвейер на заводе: каждая операция ждет завершения предыдущей. Но часто разработчики делают ошибки, которые ломают всю цепочку. Давайте посмотрим на правильный подход.

// Плохо: вложенные промисы
fetch('/user')
  .then(response => response.json())
  .then(user => {
    fetch('/posts?userId=' + user.id)
      .then(response => response.json())
      .then(posts => {
        console.log(posts);
      });
  });

// Хорошо: плоская цепочка
fetch('/user')
  .then(response => response.json())
  .then(user => fetch('/posts?userId=' + user.id))
  .then(response => response.json())
  .then(posts => console.log(posts));

Во втором случае каждый .then() возвращает новый промис, и цепочка остается плоской. Это проще читать и отлаживать. Запомните: если .then() возвращает промис, следующий .then() дождется его выполнения.

Правило: всегда возвращайте промис из .then(), чтобы цепочка оставалась плоской. Иначе вы рискуете скатиться в «ад колбэков».

Комбинирование промисов: Promise.all, race, allSettled

Иногда нужно запустить несколько асинхронных операций одновременно. Например, загрузить данные о пользователе и его постах. Для этого есть специальные методы.

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

Вот пример с Promise.all:

const userPromise = fetch('/user').then(r => r.json());
const postsPromise = fetch('/posts').then(r => r.json());

Promise.all([userPromise, postsPromise])
  .then(([user, posts]) => {
    console.log(user, posts);
  })
  .catch(error => {
    console.error('Что-то пошло не так:', error);
  });

А если вы не хотите, чтобы одна ошибка рушила всё, используйте allSettled:

Promise.allSettled([userPromise, postsPromise])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('Успех:', result.value);
      } else {
        console.log('Ошибка:', result.reason);
      }
    });
  });

Отмена промисов: как убить зависший запрос

Стандартные промисы не умеют отменяться. Но это не значит, что нельзя сделать отмену вручную. Часто это нужно, когда пользователь ушел со страницы или передумал. Используем паттерн с флагом или AbortController.

// Используем AbortController для fetch
const controller = new AbortController();
const signal = controller.signal;

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

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

Для своих промисов можно сделать обертку с отменой:

function cancellablePromise(executor) {
  let isCancelled = false;
  const promise = new Promise((resolve, reject) => {
    executor(
      value => isCancelled ? reject({ cancelled: true }) : resolve(value),
      reason => isCancelled ? reject({ cancelled: true }) : reject(reason)
    );
  });
  return {
    promise,
    cancel() {
      isCancelled = true;
    }
  };
}

// Использование
const { promise, cancel } = cancellablePromise((resolve) => {
  setTimeout(() => resolve('Готово'), 3000);
});

promise.then(console.log).catch(err => console.log('Отменено?', err.cancelled));
cancel(); // отменяем сразу
Лайфхак: для fetch используйте AbortController — это встроенный механизм, а для своих промисов — паттерн с флагом. Не забывайте обрабатывать отмену в catch.

Обработка ошибок: как не уронить приложение

Ошибки в промисах — это как гром среди ясного неба. Если их не поймать, приложение может молча упасть. Вот несколько продвинутых приемов.

Во-первых, всегда добавляйте .catch() в конец цепочки. Но лучше использовать finally для очистки:

fetch('/data')
  .then(response => response.json())
  .then(data => process(data))
  .catch(error => console.error('Ошибка:', error))
  .finally(() => {
    // Скрыть спиннер, очистить ресурсы
    console.log('Операция завершена');
  });

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

fetch('/settings')
  .then(response => {
    if (!response.ok) throw new Error('Ошибка сети');
    return response.json();
  })
  .catch(() => ({ theme: 'light' })) // дефолтные настройки
  .then(settings => applySettings(settings));

Промисификация: превращаем колбэки в промисы

Старые API часто используют колбэки. Чтобы работать с ними в современном стиле, нужно обернуть их в промисы. Это называется «промисификация».

const fs = require('fs');

function readFilePromise(path, encoding = 'utf8') {
  return new Promise((resolve, reject) => {
    fs.readFile(path, encoding, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Использование
readFilePromise('file.txt')
  .then(content => console.log(content))
  .catch(err => console.error(err));

В Node.js уже есть promisify из модуля util:

const { promisify } = require('util');
const readFile = promisify(fs.readFile);

readFile('file.txt', 'utf8')
  .then(content => console.log(content));

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

Правило: если видите функцию с колбэками, не стесняйтесь обернуть ее в промис. Это упростит код и избавит от вложенности.

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

Иногда нужно выполнить промисы строго по очереди, а не параллельно. Например, чтобы не перегрузить сервер. Для этого можно использовать цикл с await (в асинхронной функции) или reduce с цепочкой.

// Последовательное выполнение
async function runInSequence(tasks) {
  const results = [];
  for (const task of tasks) {
    results.push(await task());
  }
  return results;
}

// Пример
const tasks = [
  () => fetch('/api/1'),
  () => fetch('/api/2'),
  () => fetch('/api/3')
];

runInSequence(tasks).then(results => console.log(results));

А если нужно ограничить параллелизм (например, максимум 2 запроса одновременно), вот такой трюк:

async function runWithLimit(tasks, limit) {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const promise = task().then(result => {
      results.push(result);
    });
    executing.push(promise);

    if (executing.length >= limit) {
      await Promise.race(executing); // ждем хотя бы один завершится
      // Убираем завершившиеся из списка
      for (let i = executing.length - 1; i >= 0; i--) {
        if (executing[i] === promise) continue;
        // Простая проверка: если промис завершен, он уже не в pending
        // Но для простоты можно просто сдвигать по индексу
      }
    }
  }
  await Promise.all(executing);
  return results;
}

Этот код сложноват, но идея ясна: используем Promise.race, чтобы дождаться освобождения слота.

Итоги: что мы узнали

Промисы — это мощный инструмент, но с подводными камнями. Мы разобрали:

  • Как строить плоские цепочки и не тонуть в вложенности.
  • Как комбинировать промисы с помощью all, race, allSettled.
  • Как отменять промисы, используя AbortController или свои обертки.
  • Как правильно обрабатывать ошибки и использовать finally.
  • Как превращать колбэки в промисы.
  • Как выполнять промисы последовательно или с ограничением параллелизма.

Теперь вы готовы писать более надежный и читаемый асинхронный код. Экспериментируйте, пробуйте разные подходы — и ваши промисы никогда не будут «висеть» без дела.

Студия WNDER