Промисы: основы
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Промис — это обещание значения, которого ещё нет: пустая коробка, куда результат положат позже. Он не ускоряет асинхронную работу, а даёт понятный способ дождаться её конца и отдельно обработать успех и ошибку.
У промиса ровно три состояния. Сразу после создания он pending — ожидание, результата нет. Дальше он один раз переходит либо в fulfilled (успех, есть значение), либо в rejected (ошибка, есть причина). Момент перехода называют «улаживанием».
| Состояние | Когда наступает | Что доступно |
|---|---|---|
| pending | сразу после создания | ничего, результата ещё нет |
| fulfilled | вызвали resolve(значение) | значение приходит в then |
| rejected | вызвали reject(ошибку) или внутри бросили исключение | причина приходит в catch |
Обратной дороги нет: из fulfilled в rejected промис не вернётся, и в ожидание — тоже. Одно состояние, один результат, навсегда.
Конструктор принимает одну функцию — её называют исполнителем. Движок сам передаёт в неё два аргумента: resolve для успеха и reject для ошибки. Внутри вы запускаете свою асинхронную работу и в нужный момент дёргаете одну из этих функций.
В resolve передают любое значение, в reject — по договорённости объект Error: у него есть message и стек вызовов, с ним отладка в разы проще, чем со строкой.
Важная деталь: исполнитель запускается сразу и синхронно, прямо в момент new Promise. Асинхронным оказывается не он, а вызов обработчиков — они всегда откладываются на потом.
then подписывается на успех, catch — на ошибку, finally — на любой исход. Каждый из них возвращает новый промис, поэтому их можно ставить цепочкой: то, что вернул обработчик, становится значением для следующего звена. Готовый улаженный промис создаётся коротко — Promise.resolve(значение).
Забыли return внутри then — следующий then получит undefined. Самая частая опечатка в цепочках.
Ошибка «проваливается» вниз по цепочке до первого catch: промежуточные then просто пропускаются. А finally не получает аргументов и ничего не меняет — он для уборки: спрятать спиннер, закрыть соединение.
Даже если промис уже улажен, обработчик из then не вызовется сию секунду. Он попадает в очередь микрозадач и выполнится, когда текущий синхронный код полностью отработает. Это сделано ради предсказуемости: then ведёт себя одинаково и для готового, и для ещё ждущего промиса.
У промиса нет методов cancel или abort. Подписка — это только чтение результата, а не управление работой. Если вы передумали и просто не подписались, работа всё равно доедет до конца: таймер сработает, запрос уйдёт на сервер, ответ придёт.
Отменять нужно источник, а не обёртку: для таймера — clearTimeout, для сетевого запроса — AbortController. Промис при этом обычно завершают через reject, чтобы вызывающий код понял: результата не будет.
AbortController — это отдельный объект с сигналом. Сигнал отдают тому, кто делает работу (например, fetch), и по вызову abort() работа прекращается, а промис завершается ошибкой.
Внутри исполнителя можно вызвать resolve и reject сколько угодно раз — сработает только первый. Остальные вызовы движок игнорирует молча: ни ошибки, ни предупреждения не будет. Это защита от гонок, а не повод писать лишние вызовы.
Обратная сторона того же правила приятная: результат никуда не пропадает. Подписаться можно в любой момент — хоть через секунду после того, как промис улажен, — и обработчик всё равно получит сохранённое значение. Событие так не умеет: опоздал на него — потерял навсегда.
- Промис живёт в одном из трёх состояний: 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 само число задержки.
Промис создаётся сразу, а resolve вызывается уже изнутри таймера — поэтому обработчик then не сработает раньше, чем истечёт задержка.
Обратите внимание: resolve передаётся в setTimeout внутри стрелочной функции. Если написать setTimeout(resolve(ms), ms), промис уладится мгновенно — вызов произойдёт прямо сейчас.
Деление через промис
Напишите функцию divide(a, b), которая возвращает промис: при обычном делении — успех со значением частного, при b === 0 — ошибка Error с текстом «деление на ноль».
Условие проверяется прямо в исполнителе: одна ветка зовёт resolve, другая — reject. Дальше вызывающий код сам решает, что подключать: then, catch или оба сразу.
После reject выполнение исполнителя не прерывается — это обычный вызов функции, а не return. Здесь спасает else; в более длинном коде пишут return reject(...).