Промисы: основы

асинхронность≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

У промиса ровно три состояния. Сразу после создания он pending — ожидание, результата нет. Дальше он один раз переходит либо в fulfilled (успех, есть значение), либо в rejected (ошибка, есть причина). Момент перехода называют «улаживанием».

СостояниеКогда наступаетЧто доступно
pendingсразу после созданияничего, результата ещё нет
fulfilledвызвали resolve(значение)значение приходит в then
rejectedвызвали reject(ошибку) или внутри бросили исключениепричина приходит в catch
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратной дороги нет: из fulfilled в rejected промис не вернётся, и в ожидание — тоже. Одно состояние, один результат, навсегда.

Конструктор принимает одну функцию — её называют исполнителем. Движок сам передаёт в неё два аргумента: resolve для успеха и reject для ошибки. Внутри вы запускаете свою асинхронную работу и в нужный момент дёргаете одну из этих функций.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

В resolve передают любое значение, в reject — по договорённости объект Error: у него есть message и стек вызовов, с ним отладка в разы проще, чем со строкой.

Важная деталь: исполнитель запускается сразу и синхронно, прямо в момент new Promise. Асинхронным оказывается не он, а вызов обработчиков — они всегда откладываются на потом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Забыли return внутри then — следующий then получит undefined. Самая частая опечатка в цепочках.

Ошибка «проваливается» вниз по цепочке до первого catch: промежуточные then просто пропускаются. А finally не получает аргументов и ничего не меняет — он для уборки: спрятать спиннер, закрыть соединение.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Даже если промис уже улажен, обработчик из then не вызовется сию секунду. Он попадает в очередь микрозадач и выполнится, когда текущий синхронный код полностью отработает. Это сделано ради предсказуемости: then ведёт себя одинаково и для готового, и для ещё ждущего промиса.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

У промиса нет методов cancel или abort. Подписка — это только чтение результата, а не управление работой. Если вы передумали и просто не подписались, работа всё равно доедет до конца: таймер сработает, запрос уйдёт на сервер, ответ придёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Отменять нужно источник, а не обёртку: для таймера — clearTimeout, для сетевого запроса — AbortController. Промис при этом обычно завершают через reject, чтобы вызывающий код понял: результата не будет.

AbortController — это отдельный объект с сигналом. Сигнал отдают тому, кто делает работу (например, fetch), и по вызову abort() работа прекращается, а промис завершается ошибкой.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Внутри исполнителя можно вызвать resolve и reject сколько угодно раз — сработает только первый. Остальные вызовы движок игнорирует молча: ни ошибки, ни предупреждения не будет. Это защита от гонок, а не повод писать лишние вызовы.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратная сторона того же правила приятная: результат никуда не пропадает. Подписаться можно в любой момент — хоть через секунду после того, как промис улажен, — и обработчик всё равно получит сохранённое значение. Событие так не умеет: опоздал на него — потерял навсегда.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • Промис живёт в одном из трёх состояний: pending, fulfilled или rejected — и переходит из ожидания в исход ровно один раз.
  • new Promise принимает исполнителя с аргументами resolve и reject; сам исполнитель запускается сразу и синхронно.
  • then забирает значение, catch — ошибку, finally выполняется при любом исходе и не меняет результат; все три возвращают новый промис и складываются в цепочку.
  • Отменить промис нельзя: подписка только читает результат. Останавливать нужно источник — clearTimeout, AbortController.
  • Результат фиксируется навсегда: повторные resolve и reject игнорируются, а поздний подписчик всё равно получит сохранённое значение.

1/3Что выведет промис, в исполнителе которого подряд идут resolve("a") и resolve("b"), если подписаться через then?

2/3Что делает .finally(fn)?

3/3Функция сделала запрос и вернула промис, но результат нам больше не нужен. Что будет, если просто не подписываться?

Функция delay(ms)

Напишите функцию delay(ms), которая возвращает промис. Промис должен улаживаться через ms миллисекунд и передавать в then само число задержки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Промис создаётся сразу, а resolve вызывается уже изнутри таймера — поэтому обработчик then не сработает раньше, чем истечёт задержка.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание: resolve передаётся в setTimeout внутри стрелочной функции. Если написать setTimeout(resolve(ms), ms), промис уладится мгновенно — вызов произойдёт прямо сейчас.

Деление через промис

Напишите функцию divide(a, b), которая возвращает промис: при обычном делении — успех со значением частного, при b === 0 — ошибка Error с текстом «деление на ноль».

Условие проверяется прямо в исполнителе: одна ветка зовёт resolve, другая — reject. Дальше вызывающий код сам решает, что подключать: then, catch или оба сразу.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

После reject выполнение исполнителя не прерывается — это обычный вызов функции, а не return. Здесь спасает else; в более длинном коде пишут return reject(...).