Перейти к содержимому
Теория · Асинхронность · урок 3 из 12

Промисы

Результат таймера можно использовать только внутри колбэка. Отдать его наружу через return не получится: когда функция заканчивается, результата ещё нет. Давайте посмотрим:

JavaScript
function loadName() {
    setTimeout(() => {
        return "Ann";
    }, 100);
}

console.log(loadName()); // undefined

return "Ann" выходит из колбэка, а не из loadName, и это происходит через 100 миллисекунд. Сама loadName до конца доходит сразу и без return, поэтому возвращает undefined.

Представьте кафе, где заказ готовят не сразу. На кассе вам отдают не блюдо, а электронный пейджер. С ним можно сесть за столик и заняться своим. Когда блюдо будет готово, пейджер загорится. Если блюдо закончилось, он покажет, что заказ отменён.

Объект на будущее

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

JavaScript
function loadName() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Ann");
            resolve("Bob");
        }, 100);
    });
}

loadName().then((name) => {
    console.log(name);
});
// Ann

new Promise принимает функцию с двумя параметрами и запускает её сразу. Параметры получают две функции:

  • вызов resolve(value) выполняет промис со значением;
  • вызов reject(reason) отклоняет его с причиной, обычно объектом Error.

Три состояния

У промиса три состояния:

  • pending — промис ждёт, пейджер молчит;
  • fulfilled — промис выполнен со значением, блюдо готово;
  • rejected — промис отклонён с причиной, заказ отменён.

Сначала промис pending. Потом он становится fulfilled со значением или rejected с причиной и больше не меняется.

В примере промис остаётся pending 100 миллисекунд, пока не сработает таймер. Второй вызов resolve("Bob") ничего не делает: промис уже выполнен со значением "Ann".

Здесь аналогия с пейджером перестаёт работать. Пейджер в кафе потом отдают следующему гостю, а промис не переиспользуют: он выполняется или отклоняется один раз и навсегда.

Этот вопрос часто задают на собеседовании: что такое промис и какие у него состояния? Хороший ответ называет все три состояния и добавляет, что после pending состояние меняется только один раз.

then и catch

Метод then добавляет колбэк для значения, а catch — для причины отказа. Программу они не останавливают: только запоминают колбэк, и строки после них выполняются сразу.

JavaScript
function checkAge(age) {
    return new Promise((resolve, reject) => {
        if (age >= 0) {
            resolve(age);
        } else {
            reject(new Error("bad age"));
        }
    });
}

checkAge(-1)
    .then((age) => age + 1)
    .then((next) => console.log(next))
    .catch((e) => console.log(e.message));
// bad age

Цепочку вызовов, как в уроке о map и filter, можно разбить на строки: строка, которая начинается с точки, продолжает предыдущую. Четыре строки от checkAge(-1) до catch — одно выражение.

Каждый then возвращает новый промис со значением, которое вернул его колбэк. Так шаги выстраиваются в цепочку: следующий then получает то, что вернул предыдущий.

Разберём по шагам, что происходит при checkAge(-1):

  1. Число -1 меньше нуля, поэтому checkAge отклоняет промис с ошибкой bad age.
  2. Первый then пропускает свой колбэк и передаёт отказ дальше по цепочке.
  3. Второй then тоже пропускает свой колбэк.
  4. catch получает ошибку и печатает bad age.

С checkAge(20) всё иначе: промис выполнится со значением 20, первый then вернёт 21, второй напечатает 21, а catch не вызовется. Одного catch в конце хватает на всю цепочку.

Причина отказа приходит в колбэк catch параметром e, как в try…catch, и e.message даёт её текст. Отказ, который никто не поймал, Node.js считает ошибкой и останавливает программу. Браузер пишет его в консоль.

Частая ошибка

С промисом обращаются как с готовым значением:

JavaScript
function loadName() {
    return new Promise((resolve) => {
        resolve("ann");
    });
}

const name = loadName();
console.log(name.toUpperCase()); // ✗
TEXT
TypeError: name.toUpperCase is not a function

В name лежит промис, а не строка, и метода toUpperCase у него нет. Это всё равно что пытаться съесть пейджер. Значение достают в колбэке: loadName().then((name) => console.log(name.toUpperCase())) печатает ANN.

Главное

  • Промис — объект для результата, который будет готов позже; функция возвращает его сразу.
  • Промис pending, пока не станет fulfilled со значением или rejected с причиной, и потом больше не меняется.
  • then и catch только добавляют колбэки, а каждый then возвращает новый промис для следующего шага цепочки.
  • Отказ пропускает все then до ближайшего catch.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом