Understanding Promises

Photo by Ilya Pavlov on Unsplash

Understanding Promises

Promises are a new feature of ES6. It’s a method to write asynchronous code. It can be used when, for example, we want to fetch data from an API, or when we have a function that takes time to be executed.

The Promise Object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

A promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.

Instead of passing like this:

createAudioFileAsync(audioSettings, successCallback, failureCallback);
// NOTE: successCallback and failureCallback are callback functions.

we would attach like this:

createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

The .then() method takes up to two arguments; the first argument is a callback function for the resolved case of the promise, and the second argument is a callback function for the rejected case. Each .then() returns a newly generated promise object.

A Promise is in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

Promise chain is one of the great things about using promises.

If we were to use 2 or more asynchronous operations, where the subsequent operation starts when the previous operation succeeds, we can simply use the .then() function, as it returns a new promise.

// Before ES6: classic callback pyramid of doom
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

// ES6:
doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

// ES6: with arrow functions
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback).

From ECMAScript 2017, async/await is used as a wrapper (syntactic sugar) of promises.

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

Whenever a promise is rejected, one of two events is sent to the global scope:

  • rejectionhandled: Sent when a promise is rejected, after that rejection has been handled by the executor's reject function.
  • unhandledrejection: Sent when a promise is rejected but there is no rejection handler available.

In both cases, the event has a promise property indicating the promise that was rejected and a reason property that provides the reason given for the promise to be rejected.

// Creating a Promise around setTimeout:
setTimeout(() => saySomething("10 seconds passed"), 10*1000);

// Using Promise:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10*1000).then(() => saySomething("10 seconds")).catch(failureCallback);

Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively.

Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

Promise.all():

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

// NOTE: However, even if one promise fail, Promise.all() will throw error and abort other calls. 
// To avoid this, we can use Promise.allSettled(), which ensures all operations are complete (fulfilled or rejected) before resolving.

Promise callbacks are handled as a MicroTask and it has more priority than task queues.

const promise = new Promise(function(resolve, reject) {
  console.log("Promise callback");
  resolve();
}).then(function(result) {
  console.log("Promise callback (.then)");
});

setTimeout(function() {
  console.log("event-loop cycle: Promise (fulfilled)", promise)
}, 0);

console.log("Promise (pending)", promise);

// Output:
// Promise callback
// Promise (pending) Promise {<pending>}
// Promise callback (.then)
// event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}

// Bad example!
doSomething().then(function(result) {
  doSomethingElse(result) // Forgot to return promise from inner chain + unnecessary nesting
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Forgot to terminate chain with a catch!

// Good example!
doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.error(error));

Few other methods of Promise:

Promise.any(): It takes an iterable of Promise objects. It returns a single promise that resolves as soon as any of the promises in the iterable fulfills, with the value of the fulfilled promise. If no promises in the iterable fulfill (if all of the given promises are rejected), then the returned promise is rejected with an AggregateError, a new subclass of Error that groups together individual errors.

Syntax:

Promise.any(iterable)

Return value

  • If iterable passed is empty, then rejected Promise
  • If iterable passed contains no promises, then asynchronously resolved Promise.
  • A pending Promise in all other cases. The returning Promise is either resolved/rejected asynchronously.

This method is useful for returning the first Promise that fulfills. Unlike Promise.all(), which returns an array of fulfillment values, we only get one fulfillment value(assuming at least one promise fulfills). This can be beneficial if we need only one promise to fulfill but we do not care which one does.

Also unlike Promise.race(), which returns the first settled value (either fulfillment or rejection), this method returns the first fulfilled value. This method will ignore all rejected promises up until the first promise that fulfills.

Example:
const pErr = new Promise((resolve, reject) => {
  reject("Always fails");
});

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "Done eventually");
});

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "Done quick");
});

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value); // pFast fulfills first
})
// expected output: "Done quick"

// Promise.any() rejects with an AggregateError if no promise fulfills.
Example:
const pErr = new Promise((resolve, reject) => {
  reject('Always fails');
});

Promise.any([pErr]).catch((err) => {
  console.log(err);
})
// expected output: "AggregateError: No Promise in Promise.any was resolved"

Promise.prototype.finally(): The finally() method returns a Promise. When the promise is finally either fulfilled or rejected, the specified callback function is executed. This provides a way for code to be run whether the promise was fulfilled successfully, or instead rejected. This helps to avoid duplicating code in both the promise's then() and catch() handlers.

Few use cases:

  • Unlike Promise.resolve(2).then(() => {}, () => {}) (which will be resolved with undefined), Promise.resolve(2).finally(() => {}) will be resolved with 2.

  • Similarly, unlike Promise.reject(3).then(() => {}, () => {}) (which will be fulfilled with undefined), Promise.reject(3).finally(() => {}) will be rejected with 3.

Ciao!