Promise

Promise

Технический фундамент Promise-цепочек

Promise задает строгий контракт распространения результата:

  • каждый then получает результат предыдущего шага;
  • брошенная ошибка в любом then переводит цепочку в rejected;
  • ближайший catch перехватывает ошибку всей цепочки;
  • finally не меняет значение цепочки без явного throw/return Promise.reject(...).

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

Зачем появился Promise

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

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

Проверь себя: почему Promise удобнее callback при нескольких последовательных шагах?

Состояния Promise

У Promise есть три состояния:

  • pending - операция еще выполняется;
  • fulfilled - успешно завершилась;
  • rejected - завершилась ошибкой.

После перехода в fulfilled или rejected состояние уже не меняется.

const promise = new Promise((resolve, reject) => {
  const isOk = true;

  if (isOk) resolve('Готово');
  else reject(new Error('Сбой'));
});

promise.then((value) => console.log(value)).catch((error) => console.log(error.message));

Смотри, что важно: resolve и reject это не "return", а сигналы завершения асинхронной операции.

Смотри, что важно: функция-исполнитель (executor) внутри new Promise((resolve, reject) => { ... }) запускается сразу, синхронно. Если внутри executor случится throw, Promise станет rejected.

new Promise(() => {
  throw new Error('boom');
}).catch((error) => console.log('rejected:', error.message));

then, catch, finally

  • then обрабатывает успешный результат;
  • catch обрабатывает ошибку;
  • finally выполняется в любом случае.
fetchData()
  .then((data) => transformData(data))
  .then((result) => console.log(result))
  .catch((error) => console.error('Ошибка:', error.message))
  .finally(() => console.log('Запрос завершен'));

Здесь часто путаются: если в одном then произошла ошибка, управление перескочит в ближайший catch.

Смотри, что важно: catch не только "ловит" ошибку, но и может восстановить цепочку, если вернуть значение.

Promise.reject(new Error('fail'))
  .catch(() => 'fallback')
  .then((value) => console.log(value)); // fallback

Смотри, что важно: finally удобно использовать для cleanup (например, убрать loading). Он не получает значение/ошибку и не должен "менять" результат цепочки. Но если из finally сделать throw, цепочка станет rejected.

Проверь себя: почему один общий catch в конце цепочки часто удобнее нескольких локальных?

Возврат значения из then

Если ты возвращаешь значение из then, оно попадет в следующий then.

Promise.resolve(5)
  .then((num) => num * 2)
  .then((num) => `Результат: ${num}`)
  .then((text) => console.log(text));

Если вернуть новый Promise, цепочка подождет его завершения.

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

Promise.all и параллельные операции

Когда несколько независимых запросов можно выполнить одновременно, используют Promise.all.

Promise.all([loadUser(), loadSettings(), loadNotifications()])
  .then(([user, settings, notifications]) => {
    console.log(user, settings, notifications.length);
  })
  .catch((error) => {
    console.error('Один из запросов упал:', error.message);
  });

Ключевой момент: Promise.all завершится ошибкой, если хотя бы один Promise rejected.

Смотри, что важно:

  • Promise.all "падает быстро" при первом rejected, но остальные операции не отменяются автоматически.
  • Если нужно дождаться результатов всех операций (даже упавших), используй Promise.allSettled.
  • Если важно "кто завершился первым" (успех или ошибка), используй Promise.race.
  • Если нужен первый успешный результат, используй Promise.any (а если все упали, будет ошибка-агрегат).

Микро-сценарии применения

  1. Экран профиля: параллельно загрузить данные пользователя и настройки.
  2. Каталог товаров: получить список, затем подгрузить метаданные и объединить.

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

Типичные ошибки новичков

  • Забывать return внутри then, из-за чего цепочка теряет данные.
  • Писать catch слишком рано и "глушить" ошибку.
  • Смешивать callback и Promise без четкого контракта.
  • Считать, что Promise выполняется синхронно.
let value = 0;
Promise.resolve(10).then((v) => {
  value = v;
});

console.log(value); // 0, потому что then выполнится позже

Анти-провал: все, что зависит от результата Promise, размещай в then-цепочке или в async/await (следующий урок).

Что будет, если изменить входные данные

Если в new Promise(...) поменять isOk на false, код пойдет в catch, а then-обработчик успеха не выполнится. Это и есть контракт: либо успех, либо ошибка.

Проверь себя: выполнится ли finally, если был rejected? Почему?

Дополнительный пример: ошибка в середине цепочки и единый catch.

Promise.resolve('start')
  .then(() => {
    throw new Error('Ошибка на шаге обработки');
  })
  .catch((error) => console.log('Поймали:', error.message));

Краткий итог

  • Promise представляет будущий результат асинхронной операции.
  • Основные состояния: pending, fulfilled, rejected.
  • Цепочки then делают последовательную обработку читаемой.
  • catch централизует обработку ошибок, finally выполняется всегда.
  • Promise.all полезен для параллельных задач, но падает при ошибке любого участника.