Промис в JavaScript — это заглушка для будущего значения
Когда JavaScript берётся за операцию, требующую времени — запрос к серверу, чтение файла, ожидание таймера — он не может тут же вернуть результат. Вместо этого он отдаёт промис: объект, который представляет значение, появляющееся когда-нибудь потом.
Первый console.log выводит промис в состоянии pending. Через полсекунды промис разрешается, и колбэк в .then получает значение. Сам по себе промис — это обычный объект, но вся его магия в том, что он умеет сообщать подписчикам о появлении значения.
Три состояния промиса
Промис в JavaScript всегда находится в одном из трёх состояний:
- pending — работа ещё идёт. Значения пока нет.
- fulfilled — работа завершилась успешно. Есть результат.
- rejected — работа завершилась с ошибкой. Есть объект ошибки.
Из состояния pending промис ровно один раз переходит в fulfilled или rejected — и остаётся там навсегда. Отменить разрешение нельзя, разрешить дважды — тоже.
Promise.resolve(value) создаёт уже выполненный промис, а Promise.reject(error) — уже отклонённый. Удобно для тестов и для функций, которые иногда могут вернуть результат сразу, но всё равно должны отдавать промис.
Получение значения: .then и .catch
Достать значение из промиса напрямую нельзя — нужно передать колбэк в .then, и промис сам вызовет его, когда результат будет готов:
.catch(fn) срабатывает, если промис отклонён. По сути, это сокращение для .then(undefined, fn). Один .catch() в конце цепочки ловит ошибки с любого шага выше — не нужно навешивать его после каждого .then.
Цепочка промисов в JavaScript: каждый .then возвращает новый промис
Вот здесь многие и спотыкаются. .then() не просто выполняет колбэк — он возвращает новый промис, который разрешается в то значение, которое вернул колбэк. Именно это и позволяет строить цепочки промисов:
Каждый шаг передаёт результат следующему. Если колбэк в .then возвращает промис, цепочка дождётся именно его перед тем, как двигаться дальше — так асинхронные шаги красиво складываются в цепочку:
Три асинхронных шага подряд — и никакой вложенности. Сравните это с той же логикой на колбэках, и станет понятно, почему промисы в JavaScript так быстро прижились.
Ошибки проваливаются по цепочке
Отклонённый промис пропускает все .then подряд, пока не наткнётся на .catch. По сути, это и есть вся модель обработки ошибок в промисах:
Если внутри .then выбросить исключение, то промис, который вернул этот .then, перейдёт в состояние rejected. Следующий .then увидит ошибку и просто передаст её дальше по цепочке, пока её не поймает .catch. Обычно достаточно одного .catch в конце цепочки — а если .catch нет вообще, то в консоли появится предупреждение "unhandled promise rejection", и его точно стоит починить.
Как создать свой промис через new Promise
В большинстве случаев вы работаете с готовыми промисами, которые возвращают библиотеки. Но иногда нужно обернуть то, что промис не возвращает — обычно это старые API на колбэках:
Функция, которую вы передаёте в new Promise, называется executor (исполнитель). Она получает два аргумента: resolve (вызываем с успешным результатом) и reject (вызываем с ошибкой). Нужно вызвать ровно один из них, и ровно один раз. Все последующие вызовы будут проигнорированы.
Пара привычек, которые сэкономят вам нервы:
- Используйте
new Promiseтолько тогда, когда оборачиваете что-то, что ещё не работает через промисы. Если функция уже возвращает Promise — просто верните его как есть. - В
rejectвсегда передавайте объектError, а не строку. Стектрейс — штука полезная, не стоит его терять.
Параллельное выполнение промисов: Promise.all
Цепочка .then выполняется последовательно. Но если у вас есть несколько независимых асинхронных задач и хочется запустить их одновременно — на помощь приходит Promise.all:
Все три таймера работают параллельно. Promise.all возвращает массив результатов в том же порядке, в котором промисы были переданы — но только после того, как каждый из них успешно завершится. В итоге общее время — около 400 мс, а не 900 мс.
Есть нюанс: Promise.all отклоняется сразу же, как только любой из промисов уйдёт в reject, а остальные результаты при этом теряются. Это ровно то поведение, которое нужно, когда вам важны все данные разом (например, чтобы отрендерить страницу, для которой нужны три запроса к API). Если же это не ваш случай — берите allSettled.
Когда часть ошибок допустима: Promise.allSettled
Promise.allSettled дожидается завершения всех промисов — неважно, успешного или нет — и отдаёт подробный отчёт по каждому:
Каждый результат — это объект вида { status: "fulfilled", value } или { status: "rejected", reason }. Удобно, когда допустим частичный успех: пишем батч событий в лог, подгружаем кучу превьюшек, гоняем независимые health-чеки.
Ещё пара комбинаторов, которые стоит знать:
Promise.race([...])— завершается, как только завершится первый промис в массиве, без разницы — успехом или ошибкой. Полезно для таймаутов.Promise.any([...])— возвращает результат первого успешного промиса, а реджекты игнорирует. Упадёт только в том случае, если провалились вообще все.
Промисы в JavaScript всегда асинхронны
Даже если промис уже зарезолвлен, коллбэк .then всё равно вызовется асинхронно — никогда не синхронно и никогда в том же тике цикла событий:
Вывод: до, после, сразу. Коллбэк внутри .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 остаётся магией.