Menu
Русский

Promises в JavaScript: then, catch и Promise.all

Разбираемся, как устроены промисы в JavaScript: три состояния, цепочки через then и catch, параллельный запуск через Promise.all и создание своих промисов через new Promise.

Промис в JavaScript — это заглушка для будущего значения

Когда JavaScript берётся за операцию, требующую времени — запрос к серверу, чтение файла, ожидание таймера — он не может тут же вернуть результат. Вместо этого он отдаёт промис: объект, который представляет значение, появляющееся когда-нибудь потом.

index.js
Output
Click Run to see the output here.

Первый console.log выводит промис в состоянии pending. Через полсекунды промис разрешается, и колбэк в .then получает значение. Сам по себе промис — это обычный объект, но вся его магия в том, что он умеет сообщать подписчикам о появлении значения.

Три состояния промиса

Промис в JavaScript всегда находится в одном из трёх состояний:

  • pending — работа ещё идёт. Значения пока нет.
  • fulfilled — работа завершилась успешно. Есть результат.
  • rejected — работа завершилась с ошибкой. Есть объект ошибки.

Из состояния pending промис ровно один раз переходит в fulfilled или rejected — и остаётся там навсегда. Отменить разрешение нельзя, разрешить дважды — тоже.

index.js
Output
Click Run to see the output here.

Promise.resolve(value) создаёт уже выполненный промис, а Promise.reject(error) — уже отклонённый. Удобно для тестов и для функций, которые иногда могут вернуть результат сразу, но всё равно должны отдавать промис.

Получение значения: .then и .catch

Достать значение из промиса напрямую нельзя — нужно передать колбэк в .then, и промис сам вызовет его, когда результат будет готов:

index.js
Output
Click Run to see the output here.

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

Цепочка промисов в JavaScript: каждый .then возвращает новый промис

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

index.js
Output
Click Run to see the output here.

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

index.js
Output
Click Run to see the output here.

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

Ошибки проваливаются по цепочке

Отклонённый промис пропускает все .then подряд, пока не наткнётся на .catch. По сути, это и есть вся модель обработки ошибок в промисах:

index.js
Output
Click Run to see the output here.

Если внутри .then выбросить исключение, то промис, который вернул этот .then, перейдёт в состояние rejected. Следующий .then увидит ошибку и просто передаст её дальше по цепочке, пока её не поймает .catch. Обычно достаточно одного .catch в конце цепочки — а если .catch нет вообще, то в консоли появится предупреждение "unhandled promise rejection", и его точно стоит починить.

Как создать свой промис через new Promise

В большинстве случаев вы работаете с готовыми промисами, которые возвращают библиотеки. Но иногда нужно обернуть то, что промис не возвращает — обычно это старые API на колбэках:

index.js
Output
Click Run to see the output here.

Функция, которую вы передаёте в new Promise, называется executor (исполнитель). Она получает два аргумента: resolve (вызываем с успешным результатом) и reject (вызываем с ошибкой). Нужно вызвать ровно один из них, и ровно один раз. Все последующие вызовы будут проигнорированы.

Пара привычек, которые сэкономят вам нервы:

  • Используйте new Promise только тогда, когда оборачиваете что-то, что ещё не работает через промисы. Если функция уже возвращает Promise — просто верните его как есть.
  • В reject всегда передавайте объект Error, а не строку. Стектрейс — штука полезная, не стоит его терять.

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

Цепочка .then выполняется последовательно. Но если у вас есть несколько независимых асинхронных задач и хочется запустить их одновременно — на помощь приходит Promise.all:

index.js
Output
Click Run to see the output here.

Все три таймера работают параллельно. Promise.all возвращает массив результатов в том же порядке, в котором промисы были переданы — но только после того, как каждый из них успешно завершится. В итоге общее время — около 400 мс, а не 900 мс.

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

Когда часть ошибок допустима: Promise.allSettled

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

index.js
Output
Click Run to see the output here.

Каждый результат — это объект вида { status: "fulfilled", value } или { status: "rejected", reason }. Удобно, когда допустим частичный успех: пишем батч событий в лог, подгружаем кучу превьюшек, гоняем независимые health-чеки.

Ещё пара комбинаторов, которые стоит знать:

  • Promise.race([...]) — завершается, как только завершится первый промис в массиве, без разницы — успехом или ошибкой. Полезно для таймаутов.
  • Promise.any([...]) — возвращает результат первого успешного промиса, а реджекты игнорирует. Упадёт только в том случае, если провалились вообще все.

Промисы в JavaScript всегда асинхронны

Даже если промис уже зарезолвлен, коллбэк .then всё равно вызовется асинхронно — никогда не синхронно и никогда в том же тике цикла событий:

index.js
Output
Click Run to see the output here.

Вывод: до, после, сразу. Коллбэк внутри .then ждёт, пока доработает текущий синхронный код, и только потом выполняется — уже из очереди микрозадач. Именно это правило — «коллбэк промиса никогда не запускается синхронно» — и делает поведение промисов в javascript предсказуемым при смешивании с обычным кодом: синхронная часть всегда отрабатывает первой.

Что дальше: async/await

Цепочка вызовов .then — рабочий вариант, но стоит добавить три-четыре шага, и код превращается в лесенку. async/await — это синтаксический сахар поверх промисов, который позволяет писать ту же логику так, будто она синхронная: ошибки ловятся через try/catch, а промежуточные значения складываются в обычные переменные. Об этом и поговорим дальше.

Часто задаваемые вопросы

Что такое Promise в JavaScript простыми словами?

Promise — это объект-обёртка над значением, которого ещё нет. Обычно это результат асинхронной операции: запрос к серверу, чтение файла, таймер. В любой момент промис находится в одном из трёх состояний: pending (ожидание), fulfilled (успех) или rejected (ошибка). Чтобы получить итоговое значение, к промису навешивают колбэки через .then() и .catch().

В чём разница между then и catch?

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

Что делает Promise.all?

Promise.all([p1, p2, p3]) принимает массив промисов и возвращает один общий промис. Он разрешится массивом результатов, но только когда отработают все входящие промисы. Если хотя бы один из них упадёт — весь Promise.all сразу же реджектится с этой ошибкой. Если нужны результаты всех промисов независимо от их судьбы — берите Promise.allSettled.

Что лучше использовать: промисы или async/await?

Под капотом это одно и то же — async/await это синтаксический сахар над промисами. Современный код обычно чище читается с async/await, но функции всё равно возвращают Promise, ошибки ловятся через try/catch (или тот же .catch()), а для параллельного запуска по-прежнему нужен Promise.all. Так что понимание промисов — это база, без которой async/await остаётся магией.

Учитесь программировать с Coddy

НАЧАТЬ