Перейти к содержимому
Тема 18 из 21
Теория

Промисы

Зачем это нужно

В прошлой теме асинхронный результат передавали через колбэк, и при нескольких шагах получалась «лесенка» вложенных функций. Промис (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 выполняется синхронно, сразу при создании. Асинхронны только реакции на результат.

Частые ошибки

  1. Забытый return в then. .then((x) => { x * 2; }) передаст дальше undefined. Либо короткая стрелка (x) => x * 2, либо return.
  2. Промис внутри then без return. Цепочка не будет его ждать, а его ошибки никто не поймает.
  3. Нет catch. Отклонённый промис без обработчика — «необработанная ошибка», в Node.js она останавливает программу.
  4. Ждать по очереди то, что можно параллельно. Независимые загрузки запускай вместе и жди через Promise.all.
  5. Пытаться достать значение синхронно. 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) → таймеры

Пауза

Лёгкая+10 XP

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

delay(500, "ready").then((v) => console.log(v));   // через полсекунды: ready

Подключи коннектор, чтобы решать задачи

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

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.