Цепочки промисов в 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() и передавать значение по цепочке:
Самый мощный случай — возврат промиса из обработчика. Цепочка приостанавливается до разрешения этого промиса, прежде чем продолжить выполнение, — именно так организуются зависимые асинхронные операции:
Базовая цепочка промисов
Рассмотрим сценарий, в котором нужно сделать запрос к базе данных, а затем использовать его результат для следующего запроса. Каждый .then() возвращает промис следующего запроса, поэтому цепочка ждёт завершения одного запроса перед началом следующего:
Ошибка №1: забытый return («оторванная цепочка»)
Это самая распространённая ошибка при работе с цепочками промисов. Если вы запускаете асинхронную операцию внутри .then(), но забываете вернуть её промис, цепочка не ждёт результата — значение теряется, а следующий .then() выполняется немедленно с undefined. Внутренний промис становится «оторванной» цепочкой, работающей самостоятельно.
В сломанной версии ниже queryDatabase('posts') вызывается, но её промис не возвращается, поэтому второй .then() выводит undefined вместо данных о публикациях:
Добавление return восстанавливает цепочку. Теперь второй .then() ждёт запрос публикаций и получает его результат:
Совет: стрелочные функции с телом-выражением возвращают значение автоматически —
.then(r => queryDatabase(r))возвращает промис, но.then(r => { queryDatabase(r); })(с фигурными скобками) — нет.
Обработка ошибок в цепочках
Один .catch() в конце цепочки перехватывает любую ошибку — выброшенную или отклонённый промис — на любом предыдущем шаге. При возникновении ошибки цепочка пропускает все оставшиеся .then() и переходит к ближайшему .catch().
В этом примере первый запрос завершается отклонением, поэтому .then() полностью пропускается и управление переходит к .catch():
Более подробно о паттернах отклонения промисов смотрите в разделе Обработка ошибок с промисами.
.catch() в середине цепочки для восстановления после ошибки
.catch() не обязательно должен быть последним звеном. Размещённый в середине цепочки, он может обработать ошибку, вернуть резервное значение и позволить цепочке продолжить выполнение. В этом и состоит разница между восстановлением после сбоя и прерыванием всей последовательности.
В примере ниже первый шаг завершается ошибкой, но .catch() в середине цепочки предоставляет значение по умолчанию и выполнение продолжается:
.catch() в середине цепочки восстанавливает выполнение и продолжает его; терминальный .catch() — это финальная страховочная сеть для всего, что не было обработано ранее.
Очистка ресурсов с помощью .finally()
.finally() выполняется после завершения промиса — независимо от того, разрешился он или был отклонён. Он не получает аргументов и не изменяет значение, проходящее через цепочку, что делает его идеальным для очистки, которая должна выполняться в любом случае: скрытие спиннера, закрытие соединения или повторное включение кнопки.
Параллельное выполнение промисов: Promise.all
Promise.all — это не цепочка промисов: цепочка выполняется последовательно (один за другим), тогда как Promise.all запускает промисы параллельно и ждёт завершения всех. Используйте его, когда операции не зависят друг от друга и нет смысла ждать завершения одной перед началом следующей.
Метод принимает итерируемый набор промисов и возвращает единый промис, который разрешается в array их результатов в том же порядке, что и входные данные. Любое значение, не являющееся промисом (например, 42 в примере ниже), автоматически оборачивается в разрешённый промис. Если любой из входных промисов отклоняется, весь Promise.all немедленно отклоняется с этой ошибкой.
О методах Promise.all, Promise.race, Promise.allSettled и других комбинаторах смотрите в разделе Promise API.
Итоги
- Каждый
.then()возвращает новый промис; цепочка читается сверху вниз, без вложенности. - Возвращайте значения для их передачи по цепочке, и возвращайте промис из обработчика, чтобы цепочка ждала его выполнения.
- Самая распространённая ошибка — пропущенный
returnвнутри.then()— она отрывает внутреннюю асинхронную работу, и следующий шаг получаетundefined. - Один терминальный
.catch()обрабатывает ошибки с любого предыдущего шага;.catch()в середине цепочки может восстановить выполнение и продолжить его. - Используйте
.finally()для очистки ресурсов, которая должна выполняться вне зависимости от успеха или неудачи цепочки. - Используйте
Promise.allдля независимых операций, которые должны выполняться параллельно — это другой инструмент, не последовательная цепочка. - Когда вы освоитесь здесь, async/await предоставит те же возможности с синхронным синтаксисом.