W3docs

Цепочки промисов в JavaScript

Цепочки промисов в JavaScript позволяют выполнять асинхронные операции последовательно, передавая результат каждого шага следующему.

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

До появления промисов организация последовательных асинхронных операций требовала вложения колбэков в колбэки — это печально известный «ад колбэков» или «пирамида погибели»:

queryDatabase('users', (users) => {
  queryDatabase('posts', (posts) => {
    queryDatabase('comments', (comments) => {
      // deeply nested, hard to read, error handling duplicated everywhere
    });
  });
});

Цепочки превращают эту пирамиду в читаемую последовательность сверху вниз с единственным местом для обработки ошибок. На этой странице рассматривается, как на самом деле работают цепочки, самая распространённая ошибка (пропущенный return), восстановление после ошибок и очистка ресурсов. Для изучения смежного синтаксиса, построенного поверх промисов, смотрите JavaScript: async/await.

Как работают цепочки: каждый .then() возвращает новый промис

Это ключевой механизм, из которого вытекает всё остальное. .then() возвращает не исходный промис, а совершенно новый. То, в какое значение разрешится новый промис, зависит от того, что возвращает ваш обработчик:

  • Возврат обычного значения → следующий .then() получает это значение.
  • Возврат промиса → цепочка ждёт его завершения, и следующий .then() получает его разрешённое значение (не сам промис).
  • Ничего не возвращается → следующий .then() получает undefined.
  • Выброс ошибки → цепочка перепрыгивает к ближайшему .catch().

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

javascript— editable

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

Базовая цепочка промисов

Рассмотрим сценарий, в котором нужно сделать запрос к базе данных, а затем использовать его результат для следующего запроса. Каждый .then() возвращает промис следующего запроса, поэтому цепочка ждёт завершения одного запроса перед началом следующего:

javascript— editable

Ошибка №1: забытый return («оторванная цепочка»)

Это самая распространённая ошибка при работе с цепочками промисов. Если вы запускаете асинхронную операцию внутри .then(), но забываете вернуть её промис, цепочка не ждёт результата — значение теряется, а следующий .then() выполняется немедленно с undefined. Внутренний промис становится «оторванной» цепочкой, работающей самостоятельно.

В сломанной версии ниже queryDatabase('posts') вызывается, но её промис не возвращается, поэтому второй .then() выводит undefined вместо данных о публикациях:

javascript— editable

Добавление return восстанавливает цепочку. Теперь второй .then() ждёт запрос публикаций и получает его результат:

javascript— editable

Совет: стрелочные функции с телом-выражением возвращают значение автоматически — .then(r => queryDatabase(r)) возвращает промис, но .then(r => { queryDatabase(r); }) (с фигурными скобками) — нет.

Обработка ошибок в цепочках

Один .catch() в конце цепочки перехватывает любую ошибку — выброшенную или отклонённый промис — на любом предыдущем шаге. При возникновении ошибки цепочка пропускает все оставшиеся .then() и переходит к ближайшему .catch().

В этом примере первый запрос завершается отклонением, поэтому .then() полностью пропускается и управление переходит к .catch():

javascript— editable

Более подробно о паттернах отклонения промисов смотрите в разделе Обработка ошибок с промисами.

.catch() в середине цепочки для восстановления после ошибки

.catch() не обязательно должен быть последним звеном. Размещённый в середине цепочки, он может обработать ошибку, вернуть резервное значение и позволить цепочке продолжить выполнение. В этом и состоит разница между восстановлением после сбоя и прерыванием всей последовательности.

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

javascript— editable

.catch() в середине цепочки восстанавливает выполнение и продолжает его; терминальный .catch() — это финальная страховочная сеть для всего, что не было обработано ранее.

Очистка ресурсов с помощью .finally()

.finally() выполняется после завершения промиса — независимо от того, разрешился он или был отклонён. Он не получает аргументов и не изменяет значение, проходящее через цепочку, что делает его идеальным для очистки, которая должна выполняться в любом случае: скрытие спиннера, закрытие соединения или повторное включение кнопки.

javascript— editable

Параллельное выполнение промисов: Promise.all

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

Метод принимает итерируемый набор промисов и возвращает единый промис, который разрешается в array их результатов в том же порядке, что и входные данные. Любое значение, не являющееся промисом (например, 42 в примере ниже), автоматически оборачивается в разрешённый промис. Если любой из входных промисов отклоняется, весь Promise.all немедленно отклоняется с этой ошибкой.

javascript— editable

О методах Promise.all, Promise.race, Promise.allSettled и других комбинаторах смотрите в разделе Promise API.

Итоги

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

Практика

Практика
Какова цель цепочек промисов в JavaScript?
Какова цель цепочек промисов в JavaScript?
Was this page helpful?