ES6 - This I Promise you

Apr 29, 2020 · 10 min read
ES6 - This I Promise you

Promise is not only one of the most commonly used ES6 features but also a challenging concept to understand or explain, even for experienced developers. But first, we shall discuss two concepts - synchronous and asynchronous. What are they, and why are they related to Promise?

Let's check it out, shall we? 😃

Synchronous and Asynchronous - What does it mean?

When it comes to Promise in JavaScript, we often hear about terms like synchronous and asynchronous. Understanding how these concepts work is crucial for any web developers to understand Promise and write better code.

Thus before we start, let's take a quick look at them.

Synchronous

Synchronous refers to actions happening and finishing at the same timeline by a specific order (sequential). A good example of synchronous is how we queue at any service counter. The next person can only approach after the previous one has been served and completed his business at the counter.

Synchronous interactions at service counter queue

Asynchronous

On the contrary, asynchronous refers to actions that can happen in one timeline and finish in another, without respect to the current sequence order.

Take the McDonald's purchasing queue, for instance. Your turn ends once you made a purchase, but your purchase is still incomplete until you receive your food. And the process of getting your ready food doesn't have to follow the same queue order or to interrupt any other on-going transaction at the purchasing counter.

Similarly, in programming, asynchronous means action can start, run in the background without disturbing other activities to proceed, and resume later if needed.

An example of how Asynchronous action executes in the same queues with other operations

Well, JavaScript operates in a single-thread and asynchronous manner. In other words:

  • JavaScript engine executes one operation at a time.
  • If an operation is asynchronous - aka waiting for an external condition to be fulfilled (e.g., Ajax call), the engine does not stop and wait for it to finish. Instead, it continues to the next operation in the waiting queue and resumes that specific operation later if possible, once the queue is empty.

Great, so what is Promise? And how it relates to all of these?

What is Promise?

In short, a Promise is a mechanism to allow asynchronous actions synchronously return values.

A promise is a result guarantee for a particular asynchronous operation on its execution. In other words, the related action makes a promise to return a final value, regardless of how long it takes to execute. And unlike the real world, it always keeps its promise 😄.

So how do we create a promise?

Creating a promise

We can create a new promise by calling its constructor, which accepts a method (the executor), in the following syntax:

const myPromise = new Promise(executor)

The executor itself receives two methods as its arguments - resolve and reject, and asynchronously executes target operation.

const executor = (resolve,reject) => { /* operation logic */ }
  • resolve() is called once the main logic of the operation completes the execution successfully (settles) - aka it happens when the created Promise resolves.
  • reject() is called when there is an error, or unaccepted result occurred - aka it happens when the created Promise should reject.

That's the definition. It is entirely up to us to implement how we want to trigger resolve() and reject() in the executor's logic. An example of common executor with resolve() and reject() is shown as below

Executor function implementation breakdown

Cool, now that we have a Promise created. What does it look like? Let's check it out

A created Promise in console

The returned output is the created Promise, in the form of an Object. So what does this object contain?

The Promise Object

A Promise is an object that contains two fields of read-only information on a particular operation:

  • The return value of the Promise after being settled ([[PromiseValue]])
  • Its status state ([[PromiseStatus]]).

Let's look closely at the status state. There are three different states of a promise operation:

  • Fulfilled (or resolved in versions before Chrome Canary and Safari) ✅ - when the operation completed execution without error, meaning the Promise resolves (resolve() triggered).
  • Rejected 🔴 - as the opposite, when there is an error occurred during the operation, meaning when the Promise chooses to reject (reject() triggered).
  • Pending ⏳ - the operation is still in execution and not yet completed/failed.

Important note: when implementing the executor, you have to call resolve() or rejected() to indicate the status of the promise. Otherwise, the promise will always stay wrongly as pending, regardless of the actual execution result. As in the example below:

Promise is always in "pending" since no resolve() or reject() is called

Simple enough. What about the return value [[PromiseValue]]?

The return value of a promise is the value passed as an argument to the call resolve() or reject(), for instance:

The return value of Promise matches the data passed to resolve()

OK, we finally got a hang on the Promise object. Next question - what should we do once a Promise resolves/rejects?

How Promise works in chaining

As mentioned in the previous section, once a Promise resolves/rejects, it normally returns a value (passed through the call of resolve() or reject()). But how to access the return value without checking [[PromiseValue]] and continues our work with that value? By using the chaining mechanism of Promise. Promise APIs provides us with three built-in methods, as follow:

  • .then() - is called after a Promise resolves (similar to .done() in Ajax).
  • .catch() - is called after a Promise rejects (similar to .fail() in Ajax)
  • .finally() - is always called at the end, regardless the status of a Promise. This method is similar to .always() in Ajax.

Each of the above accepts a callback function as an argument, and returns a new Promise, allowing chaining to another method if needed. While the callbacks of .then() and .catch() receives the return value from the chained Promise, the callback of .finally() is simply a method without argument.

Note here that .then() receives the same value passed to resolve() and similarly, .catch() receives the value passed to reject(). For example, we have a Promise that resolves when a random generated number is <5 and rejects otherwise:

const myPromise = new Promise((resolve, reject) => {
  const randNum = generateRandom();
  if (randNum < 5) {
    resolve(`I'm resolved at ${randNum}`);
  } else {
    reject(`I'm rejected at ${randNum}`);
  }
});

With a simple chaining as below

myPromise
  .then(result => console.log(result))
  .catch(error => console.log(error))
  .finally(() => console.log("Completed"))

When myPromise resolves, the output will be:

Simple chaining in Promise when it's resolved

And when it's rejected:

Simple chaining in Promise when it's rejected

The chaining flow is seen as follows:

Chaining flow of Promise's built-in methods

Great. Now you know how Promise works in general, let's do a little quiz to prove it, shall we 😉?

The Quiz

Before we start, promise me (and honor it in JavaScript way 😉) to try answer it before copy/paste the code to the Console in Devtools? I know it's tempting, but it's cheating 😆. OK?

All the code is based on the example used in the section above.

  1. What is the output of the following code?

Question 1 - output of the chaining Promise code

  1. Now let's change the order of chaining a bit, what will be the output?

Question 2 - output of the more complex Promise code

I know 🤯. It's not as simple as it may look like (or sound like). Whether you get all (or partial) correct or not, this is not easy, and even experienced developers don't guess correctly either. But before we jump into the answers, let's dive into Promise chaining a bit deeper.

Promise chaining breakdowns

As stated previously, each Promise method returns another newly generated Promise for chaining. And the callback passed to a specific Promise method handler decides the state of the new Promise returned by that method.

In other words, if the callback executes successfully and returns a value, that value becomes the [[PromiseValue]] of the newly generated Promise. Its status [[PromiseStatus]] becomes fulfilled (or resolved) accordingly.

Otherwise, if there is any error (exception) that occurred (generally by using throw), the status will be set to rejected. And the new Promise will have the return value according to the error the throw action performs, as seen in the following example.

A new created promise with status rejected

In short, a Promise chain can be broken down into a series of sub Promises, each with dedicated handlers and is executed following the order of chaining. Take our Quiz #1, for instance. It can be re-written as follows:

Chaining in Question 1 is broken down to sub-promises

What happens if one of the sub Promises doesn't have the proper handler for a specific state? For example, a handler .then() for the firstPromise when it's fulfilled (resolved), as shown in the code above? Or no .catch() for myPromise in case it's rejected?

Well, in such a situation, the next Promise carries on what hasn't been handled in the previous Promise. Then it either handles or proceeds that unhandled status to the following promise in the chain. And the process continues until there is a proper handler to execute. 

This mechanism allows chaining to proceed safely until the last Promise handler in the chain without breaking. 

So far, so good? By now, I hope the answers for the quiz become clearer 😉. Let's check.

Quiz answers

Question 1

After re-written, we can break down the process into the diagram below.

Breaking down of Promise chaining into smaller promises - Question 1

Hence, in case myPromise resolves, the output will look similar to

Output of question 1 when myPromise resolves

And in rejection it will be:

Output of question 1 when myPromise rejects

There is no throw operation triggered besides. Thus the second .catch() handler is never triggered. 

Question 2

The chaining can be broken down, similarly to #1 as below:

Breaking down of Promise chaining into smaller promises - Question 2

That implies, in case myPromise resolves, the output is similar to:

Output of question 2 when myPromise resolves

And in rejection:

Output of question 2 when myPromise rejects

Awesome. Since we (finally) understand Promise and its chaining mechanism, are there any other Promise APIs that can be useful to us?

Let's find out.

Promise static methods

The built-in methods of Promise object work when you have a single Promise to watch. However, in reality, sometimes we have to wait for several Promises, which are not directly chained together, to complete before proceeding. Take fetching a user's information, his photos separately, for instance. Bringing the images doesn't rely on a user's personal information. Still, we need both to finish loading for a full user's data. In this case, we need the power of Promise.all() Promise.all()

The static method accepts an iterable object (array) of Promises as its argument, executes them and waits for one of the two below scenarios to happen:

  • All the Promises resolves - it will resolve with an array of values from resolved promises, in the same order as the input array. For example:
const promiseA = new Promise ((resolve, reject) => resolve('hello'))
const promiseB = new Promise ((resolve, reject) => resolve('world'))
Promise.all([promiseA, promiseB]).then(res => console.log(res))
//["hello", "world"]
  • Any of the Promises get rejected - it will reject with the value of first rejected Promise in the input array. For example:
const promiseA = new Promise ((resolve, reject) => resolve('hello'))
const promiseB = new Promise ((resolve, reject) => reject('world'))
const promiseC = new Promise ((resolve, reject) => reject('2020'))
Promise.all([promiseA, promiseB, promiseC]).catch(err => console.log(err))
//"world"

On the opposite, sometimes, we want to wait for at least one of the operations to complete and proceed. That's when Promise.race() comes in.

Promise.race()

As said, Promise.race() also accepts an iterable object (array) of Promises as its argument and executes them respectively. But unlike Promise.all(), it waits for the first resolved/rejected Promise and resolves/rejects with that Promise's return value accordingly. For example:

const promiseA = new Promise ((resolve, reject) => resolve('hello'))
const promiseB = new Promise ((resolve, reject) => resolve('world'))
Promise.race([promiseA, promiseB]).then(res => console.log(res))
//"hello"

Or 

const promiseA = new Promise ((resolve, reject) => reject('hello'))
const promiseB = new Promise ((resolve, reject) => resolve('world'))
const promiseC = new Promise ((resolve, reject) => reject('2020'))
Promise.race([promiseA, promiseB, promiseC]).catch(err => console.log(err))
//"hello"

And last but not least, Promise.resolve() and Promise.reject()

Promise.resolve() and Promise.reject()

These two static methods are useful when you want to create a Promise that always resolves/rejects with a value provided in advance as its return value. For instance:

//Instead of
const promise = new Promise(res => res('Hello'))
//We can write as
const promise = Promise.resolve('Hello')
promise.then(res => console.log(res)) //'Hello'

Or similarly another example with Promise.reject()

//Instead of
const promise = new Promise((resolve, reject) => reject('Hello'))
//We can write as
const promise = Promise.reject('Hello')
promise.catch(res => console.log(res)) //'Hello'

Cool, isn't it? And that's the basics about Promise APIs we need to know, to start working with it in harmony 💁. What are the other things related to Promise we will discuss next?

What's next?

The concept of Promise is straightforward but requires a lot of practice to master. And understanding Promise is just the beginning to understand how JavaScript engine works with all the operations, both synchronous and asynchronous in the background, especially when it is single-thread.

Since a Promise is asynchronous, how JavaScript engine decides when to trigger callback handlers once that Promise resolves/rejects, among other synchronous operations in the same thread, is significant. In the next article, we will explore the following:

  • The order of execution in JavaScript engines, 
  • How it schedules and choose a Promise's callbacks in the Task Queues, and
  • How async/await (may) differs from Promise.

See you soon and enjoy reading! ❤️

Extra Resources

Demo code playground for Promise

Promise documentation - MDN