Promise API

1. Введение

Promise (обещание, промис) — объект, представляющий текущее состояние асинхронной операции. Удобный способ организации асинхронного кода.

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

  • Pending — ожидание, исходное состояние при создании промиса.

  • Settled — выполнен, которое в свою очередь делится на две категории: fullfilled — выполнено успешно и rejected — выполнено с ошибкой.

Вначале промис находится в состоянии ожидания (pending), после чего он может выполнится успешно (fulfilled) или с ошибкой (rejected). Когда промис переходит в состояние выполнен (settled), с результатом или ошибкой – это навсегда. Грубо говоря, промис - это болванка для данных, значение которых мы не знаем в момент его создания.

promise intro

Способ использования:

  • Код, которому надо сделать что-то асинхронно, создаёт обещание и возвращает его.

  • Внешний код, получив обещание, навешивает на него обработчики.

  • По завершении процесса асинхронный код переводит обещание в состояние fulfilled или rejected. При этом автоматически вызываются обработчики во внешнем коде.

promise states

Отличия промиса и callback-функции:

  • Коллбэки - это функции, обещания это объекты.

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

  • Коллбэки обрабатывают успешное или неуспешное завершение, обещания ничего не обрабатывают.

  • Коллбэки могут обрабатывать несколько событий, обещания связаны только с одним событием.

Обещания (помимо читабельности, которая является побочным положительным эффектом) используются в основном для композиции, цепочки вызовов и обработки результата и ошибок в отдельных логических ветках.

2. Создание

Обещание создается как экземпляр класса Promise с одной функцией в качестве аргумента. Вызов конструктора немедленно исполнит функцию fn, переданную в качестве аргумента. Цель этой функции состоит в информировании экземпляра (промиса), когда событие, с которым он связан, будет завершено.

const promise = new Promise((resolve, reject) => {
  /*
   * Эта функция будет вызвана автоматически. В ней можно выполнять
   * любые асинхронные операции. Когда они завершатся — нужно
   * вызвать одно из: resolve(результат) при успешном выполнении,
   * или reject(ошибка) при ошибке.
   */
});
Copy

Параметры передаваемой функции:

  • resolve(arg) — функция, которую необходимо вызвать при успешной операции. Переданный в нее аргумент будет значением выполненного промиса.

  • reject(arg) — функция, которую необходимо вызвать при ошибке. Переданный в нее аргумент будет значением ошибки которое можно будет обработать.

creating promise
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success!');
  }, 2000);
});
Copy

В переменную promise будет записан объект обещания в состоянии pending, а через 2 секунды, после того как будет вызван resolve("success!"), промис перейдет в состояние fullfilled.

3. Использование

После того как промис создан, с ним можно работать используя методы then и catch, которые доступны через его прототип. Код пишется так, как будто мы размышляем о том, что может произойти если промис выполнится или нет, не думая о временных рамках.

3.1. then

Позволяет выполнить код, в котором можно получить доступ и обработать результат промиса.

promise.then(onResolve, onReject)
Copy

В метод then передаются две функции, которые будут вызваны когда промис перейдет в состояние выполнен (settled).

  • onResolve(arg) — будет вызвана при успешном выполнении промиса и получит результат промиса как аргумент (то, что передаем в вызов resolve).

  • onReject(arg) — будет вызвана при выполнении промиса с ошибкой и получит ошибку как аргумент (то, что передаем в вызов reject).

then
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    /*
     * Если какое-то условие выполняется, то есть все хорошо,
     * вызываем resolve и получает данные. Условие зависит от задачи.
     */
    resolve('Data passed into resolve function :)');

    // Если что-то не так, вызываем reject и передаем ошибку
    // reject("Error passed into reject function :(")
  }, 2000);
});

// Выполнится мгновенно
console.log('BEFORE promise.then');

const onResolve = data => {
  console.log('INSIDE promise.then - onResolve');
  console.log(data); // "Data passed into resolve function :)"
};

const onReject = error => {
  console.log('INSIDE promise.then - onReject');
  console.log(error); // "Error passed into reject function :("
};

promise.then(
  // будет вызвана через 2 секунды, если обещание выполнится успешно
  onResolve,
  // будет вызвана через 2 секунды, если обещание выполнится с ошибкой
  onReject,
);

// Выполнится мгновенно
console.log('AFTER promise.then');
Copy

Если onResolve и onReject не содержат сложной логики, их объявляют как инлайн функции в методе then.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // Если все ок, то вызывается resolve и передаем данные
    resolve('Data passed into resolve function :)');

    // Если что-то не так, вызваем reject и передаем ошибку
    // reject("Error passed into reject function :(")
  }, 2000);
});

// Выполнится мгновенно
console.log('BEFORE promise.then');

promise.then(
  // Будет вызвана через 2 секунды, если обещание выполнится успешно
  data => {
    console.log('INSIDE promise.then - onResolve');
    console.log(data); // "Data passed into resolve function :)"
  },
  // Будет вызвана через 2 секунды, если обещание выполнится с ошибкой
  error => {
    console.log('INSIDE promise.then - onReject');
    console.log(error); // "Error passed into reject function :("
  },
);

// Выполнится мгновенно
console.log('AFTER promise.then');
Copy

3.2. catch

Немного дальше мы узнаем о цепочках промисов, а пока научимся обрабатывать ошибки не в колбеке onReject метода then, а в специальном методе catch. Обрабатывать ошибки очень удобно, используя метод catch только один раз, в конце цепочки.

promise.catch(onReject)
Copy

Хендлер для обработки состояния reject, исполнится только если промис исполнится с ошибкой (rejected). onReject(arg) будет вызвана при выполнении промиса с ошибкой, и получит ошибку как аргумент (то, что передаем в вызов reject).

Создадим обещание, сделаем задержку на 2 секунды, вызовем reject, имитируя выполнение промиса с ошибкой.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('There was an error :(');
  }, 2000);
});

/*
 * then не выполнится так как в функции fn, внутри new Promise(fn),
 * был вызван reject(). А catch как раз выполнится через 2 секунды
 */
promise
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.log(error);
  });
Copy

3.3. finally

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

Позволяет выполнить указанную callback-функцию после того, как обещание будет разрешено (выполнено или отклонено). Позволяет избежать дублирования кода в обработчиках then() и catch(). Возвращает обещание.

promise.finally(() => {
  // settled (fulfilled или rejected)
});
Copy

Функция обратного вызова не получит никаких аргументов, поскольку нельзя точно определить выполнено ли обещание или отклонено. Тут будет выполняться код, который зависит только от времени его исполнения, значение промиса не важно.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success!');
  }, 2000);
});

promise
  .then(data => console.log(data)) // "success"
  .catch(error => console.log(error))
  .finally(() => console.log('finished!')); // "finished"
Copy

4. Цепочки промисов

Чейнинг (chaining) — возможность строить асинхронные цепочки из промисов. Одна из основных причин существования и активного использования промисов.

Каждый метод then, результатом своего выполнения, возвращает промис. Его значением будет то, что возвращается из callback-функции onResolve.

chaining
asyncFn(...)
  .then(...)
  .then(...)
  .then(...)
  .catch(...);
Copy

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

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(5);
  }, 2000);
});

promise
  .then(value => {
    console.log(value); // 5
    return value * 2;
  })
  .then(value => {
    console.log(value); // 10
    return value * 3;
  })
  .then(value => {
    console.log(value); // 30
  })
  .catch(error => {
    console.log(error);
  });
Copy

При возникновении ошибки в любом месте цепочки, выполнение всех последующих then отменяется, а управление передается методу catch.

5. Статические методы класса Promise

Иногда бывают ситуации, когда нам необходимо дождаться выполнения не одного, а сразу набора промисов и только тогда что-то сделать. Бывают и такие ситуации, когда из набора промисов необходимо дождаться выполнения любого одного, проигнорировав остальные.

5.1. Promise.all()

Статический метод, получает массив промисов и ждет их исполнения, возвращает промис.

Promise.all([promise1, promise2, ...])
Copy

При успешном выполнении всех промисов из массива, промис, возвращаемый из Promise.all, перейдет в состояние settled -> fullfilled, а его значением будет массив результатов исполнения каждого промиса.

Если в массиве промисов хотя бы один исполнился с ошибкой, то перейдет в состояние settiled -> rejected, а значением промиса будет ошибка.

Давайте напишем функцию, которая будет принимать текст для resolve и задержку в мс, а результатом своего выполнения будет возвращать промис. Затем создадим 2 промиса с разным временем задержки.

const makePromise = (text, delay) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(text), delay);
  });
};

const promiseA = makePromise('promiseA', 1000);
const promiseB = makePromise('promiseB', 3000);

/*
 * Выполнится спустя 3 секунды, когда выполнится второй промис с задержкой в 3c.
 * Первый выполнится через секунду и просто будет готов
 */
Promise.all([promiseA, promiseB])
  .then(result => console.log(result)) //["promiseA", "promiseB"]
  .catch(err => console.log(err));
Copy

5.2. Promise.race()

Promise.race([promise1, promise2, ...])
Copy

Статический метод, получает массив промисов и возвращает обещание. Когда хотя бы одно обещание в массиве исполнилось, исполнится возвращаемый промис, а все остальные будут отброшены.

const makePromise = (text, delay) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(text), delay);
  });
};

const promiseA = makePromise('promiseA', 1000);
const promiseB = makePromise('promiseB', 3000);

/*
 * Выполнится спустя 1 секунду, когда выполнится самый быстрый promiseA
 * с задержкой в 1c. Второй промис promiseB будет проигнорирован
 */
Promise.race([promiseA, promiseB])
  .then(result => console.log(result)) // "promiseA"
  .catch(err => console.log(err));
Copy

6. Promise.resolve(), Promise.reject() и Promise.finally()

Вызов статического метода Promise.resolve(value) создаёт успешно выполнившийся промис с результатом value. Это аналогично new Promise((resolve) => resolve(value)), только короче. Этот метод используют, когда хотят построить асинхронную цепочку и начальный результат уже есть.

Это можно использовать для того, чтобы заменить callback на цепочку промисов. То есть вместо, того чтобы передавать callback в функцию и надеяться на лучшее, получаем промис и чейним then, в котором доступен результат работы функции.

const getMessage = function (callback) {
  const input = prompt('Введите сообщение');

  callback(input);
};

const logger = message => console.log(message);

getMessage(logger);
Copy

Превращается в следующее.

const getMessage = function () {
  const input = prompt('Введите сообщение');

  return Promise.resolve(input);
};

const logger = message => console.log(message);

getMessage().then(message => logger(message));

// Или еще короче
getMessage().then(logger);
Copy

Аналогично Promise.reject(error) создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой error. Он используется крайне редко, потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения.

7. Дополнительные материалы

Last updated