Промисы
Зачем это нужно
В прошлой теме асинхронный результат передавали через колбэк, и при нескольких шагах получалась
«лесенка» вложенных функций. Промис (Promise — «обещание») — это объект, который представляет
будущий результат. Его можно вернуть из функции через обычный return, сохранить в переменную,
передать дальше и подписаться на результат.
На промисах построен весь современный JavaScript: fetch (запросы к серверу), чтение файлов в
Node.js, async/await, загрузка данных в React. Даже если ты будешь писать await, под ним всегда
промисы — поэтому их нужно понимать.
Три состояния
Промис всегда находится в одном из трёх состояний:
| Состояние | Что значит |
|---|---|
pending (ожидание) |
результата ещё нет |
fulfilled (выполнен) |
есть результат — значение |
rejected (отклонён) |
случилась ошибка — есть причина |
Из ожидания промис переходит в одно из двух конечных состояний один раз и больше не меняется.
Создать промис: new Promise
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve("готово"), ms);
});
}
console.log("Ждём...");
delay(100).then((result) => console.log(result));
Вывод:
Ждём...
готово
- В
new Promise(…)передают функцию-«исполнитель». Она запускается сразу и получает две функции:resolve(значение)— «выполнено успешно» иreject(ошибка)— «не получилось». .then(колбэк)— «когда будет результат, вызови колбэк с ним».
Пример с ошибкой:
function checkAge(age) {
return new Promise((resolve, reject) => {
if (age >= 18) resolve("Доступ открыт");
else reject(new Error("Слишком рано"));
});
}
checkAge(20).then((text) => console.log(text));
checkAge(12)
.then((text) => console.log(text))
.catch((err) => console.log("Ошибка: " + err.message));
Вывод:
Доступ открыт
Ошибка: Слишком рано
.catch(колбэк) ловит отклонение. В reject принято передавать объект Error, как и в throw.
finally
.finally(колбэк) выполняется в любом случае — удачном или нет. Например, чтобы спрятать
индикатор загрузки:
checkAge(12)
.then((text) => console.log(text))
.catch((err) => console.log("Ошибка: " + err.message))
.finally(() => console.log("Загрузка закончена"));
Цепочки then
Главная сила промисов: then сам возвращает новый промис. Что вернул колбэк — тем и выполнится
следующий промис. Так шаги идут друг за другом без вложенности:
Promise.resolve(2)
.then((x) => x * 10) // 20
.then((x) => x + 1) // 21
.then((x) => console.log("Итог: " + x));
Вывод:
Итог: 21
Если колбэк возвращает промис, цепочка подождёт его:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
wait(50, "Аня")
.then((name) => wait(50, name + " из Душанбе")) // ждём ещё один промис
.then((text) => console.log(text));
Вывод:
Аня из Душанбе
Сравни с «адом колбэков» из прошлой темы: шаги записаны плоским списком.
Ошибки в цепочке
Ошибка (через reject или обычный throw внутри then) пропускает все следующие then и
попадает в ближайший catch. Поэтому одного catch в конце хватает на всю цепочку:
Promise.resolve(1)
.then((x) => {
throw new Error("сломалось на шаге 2");
})
.then((x) => console.log("этот шаг пропущен"))
.catch((err) => console.log("Поймали: " + err.message))
.then(() => console.log("После catch цепочка продолжается"));
Вывод:
Поймали: сломалось на шаге 2
После catch цепочка продолжается
catch тоже возвращает промис: то, что он вернул, становится новым значением. Так подставляют
значение по умолчанию: .catch(() => []).
Несколько промисов сразу
Часто нужно запустить несколько задач параллельно и дождаться всех. Для этого есть статические
методы Promise:
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const start = Date.now();
Promise.all([wait(100, "A"), wait(100, "B"), wait(100, "C")]).then((values) => {
console.log(values);
console.log("Быстрее 200 мс: " + (Date.now() - start < 200));
});
Вывод:
["A","B","C"]
Быстрее 200 мс: true
Три ожидания по 100 мс заняли примерно 100 мс, а не 300: они шли одновременно.
| Метод | Когда выполняется | Результат |
|---|---|---|
Promise.all(arr) |
все выполнены | массив значений в порядке массива; при первой ошибке — отклонён |
Promise.allSettled(arr) |
все завершились, как угодно | массив { status, value } или { status, reason } |
Promise.race(arr) |
первый завершился | его значение или ошибка |
Promise.any(arr) |
первый успешный | его значение |
allSettled удобен, когда одна неудача не должна ломать всё:
Promise.allSettled([Promise.resolve(1), Promise.reject(new Error("нет")), Promise.resolve(3)]).then((results) => {
console.log(results.map((r) => (r.status === "fulfilled" ? r.value : "ошибка")));
});
Вывод:
[1,"ошибка",3]
А race — способ поставить ограничение по времени: «если ответ не пришёл за 3 секунды — ошибка».
Как это работает внутри: микрозадачи
Колбэки then, catch и finally никогда не вызываются сразу, даже если промис уже выполнен. Они
попадают в особую очередь — микрозадачи. Её цикл событий разбирает сразу после текущего кода и
раньше обычных задач вроде таймеров:
console.log("1: синхронный код");
setTimeout(() => console.log("4: таймер"), 0);
Promise.resolve().then(() => console.log("3: then"));
console.log("2: синхронный код");
Вывод:
1: синхронный код
2: синхронный код
3: then
4: таймер
Порядок такой: весь синхронный код → все микрозадачи (промисы) → следующая задача из очереди (таймер, клик) → снова микрозадачи → …
Ещё одно важное свойство: функция-исполнитель в new Promise выполняется синхронно, сразу при
создании. Асинхронны только реакции на результат.
Частые ошибки
- Забытый
returnвthen..then((x) => { x * 2; })передаст дальшеundefined. Либо короткая стрелка(x) => x * 2, либоreturn. - Промис внутри
thenбезreturn. Цепочка не будет его ждать, а его ошибки никто не поймает. - Нет
catch. Отклонённый промис без обработчика — «необработанная ошибка», в Node.js она останавливает программу. - Ждать по очереди то, что можно параллельно. Независимые загрузки запускай вместе и жди через
Promise.all. - Пытаться достать значение синхронно.
const x = delay(100);— вxлежит промис, а не результат. Результат доступен только вthen(или черезawait, следующая тема).
Шпаргалка
| Что | Как |
|---|---|
| Создать | new Promise((resolve, reject) => { ... }) |
| Готовый промис | Promise.resolve(x), Promise.reject(err) |
| Результат | .then((value) => ...) |
| Ошибка | .catch((err) => ...) |
| В любом случае | .finally(() => ...) |
| Ждать все | Promise.all([...]) |
| Все, даже с ошибками | Promise.allSettled([...]) |
| Кто первый | Promise.race([...]) |
| Порядок | синхронный код → микрозадачи (then) → таймеры |