Understanding JavaScript Promise

The JavaScript Promise is a concept that every modern self-respecting web developer should be familiar with. No matter if you just started learning JavaScript or trying to catch up on the JavaScript language updates. If you want to understand the JavaScript Promise, this article is for you.

I will quickly take you through the theory of Promises and jump right into its API with practical usage examples. After reading, you will be ready to work with Promises created by JavaScript frameworks but also know how to create your own.

Enough of the introduction. Let’s get this show on the road.
Advertisement

What JavaScript Promise is?

Simply put, the Promise is a built-in JavaScript object which represents the result of an asynchronously executed function. The Promise is a handler to the eventual result available once the JavaScript runtime calculates it.

Because the JavaScript runtime environment runs asynchronous operations using the Event Loop, you don’t know exactly when the result is ready. For this reason, the Promise object can be in one of three possible states:

  1. Pending – The initial state which means that the result is not available yet.
  2. Fulfilled – The state set when the operation is completed and the result is available.
  3. Rejected – The state indicating that the operation was unsuccessful.

To make it simple, you can think of the JavaScript Promise as of an oven. You put a raw dish inside. Once the baking is over, you can enjoy your dinner. But if something goes wrong? Well, you probably should start calling for a pizza. 🙂

Promise of dinner

When to use JavaScript Promise?

Of course, asynchronous calls didn’t come to JavaScript with Promises. The language had the concept long before that. Prior to the Promise, we used to handle asynchronous results with so-called callback functions.

A callback function indicates the operation which JavaScript should execute once an asynchronous operation is finished. Here is the simplest example of a callback function in action:

setTimeout(function () {
   console.log('Hello');
}, 100);

Frankly, there is nothing wrong in a single callback. However, the problem starts when you try chaining multiple callbacks, like this:

setTimeout(function () {
   console.log('Hello');
   setTimeout(function () {
       console.log('JS');
       setTimeout(function () {
           console.log('developers');
       }, 200);
   }, 400);
}, 600);

The above example illustrates the problem known as a callback hell or the pyramid of doom. It makes your code hard to read and understand.

Yet, a callback hell isn’t the only problem that the JavaScript Promise tries to solve. What about combining the outcomes of multiple callbacks? I’ll return to this question later on.

But first, let’s go through a simple JavaScript Promise tutorial.

How to create JavaScript Promise?

An example is worth a thousand words. Let’s have a look on the following sample.

let everythingOk = true; // var for error simulation

let dinnerPreparation = new Promise(function (resolve, reject) { // (1)
   setTimeout(function () {
       if (everythingOk) {
           resolve('Dinner is ready!') // (2)
       } else {
           reject(new Error('Ups...')); // (3)
       }
   }, 1000);
});

What is going on here?

Since the Promise is an object, we create it by calling its constructor with the new operator (1). The constructor accepts a single parameter known as the executor. The Promise executor is a two-argument function responsible for the execution of your asynchronous code. The first argument is a function which the executor should call once its execution is successfully completed (2). The second argument is a function called on a failure (3).

But that’s not all.

This code sample hides one very important detail.

What isn’t visible in this sample is the fact that the constructor of the Promise immediately calls the executor function. Right away. Once the Promise is created, your operation is in progress.

How to return Promise result?

The Promise executor function always returns its outcome via arguments of either resolve() or reject() function. If you try returning some value using the return keyword inside the executor, the value will be ignored.

Both resolve() and reject() functions accept a single argument which represents either the successful result or the reason for the error. If you try passing more parameters, they will be also ignored.

Promise reject reason type

Although you can pass any type of a variable as a reason for a failure, it’s recommended to use an instance of Error.

Why?

Because it will help you tracking bugs in your code with small effort. JavaScript runtime environment handles Errors in a special way. For instance, it provides you with the name of the file and the exact line of code in which the problem occurs.

Here’s how the reason looks in the JavaScript console:

Error: "Ups..."
    dinnerPreparation ../js-playground/promises.js:8

As you can see, it’s not only some good practice for JavaScript purists. It’s a practical approach.

What is more, if something goes wrong while processing your Promise in the background, JavaScript runtime will also mark Promise as rejected and return the reason as Error.

Wrapping value into Promise

Sometime, you may want to represent some already computed value as Promise. The API of Promise provides us with two methods for creating Promises which are completed. Use Promise.resolve() to create a fulfilled Promise or Promise.reject() to produce a failed Promise.

let preparedDinner = Promise.resolve('Dinner is ready!');

let failedDinner = Promise.reject(new Error('Ups...'));

What can you do with JavaScript Promise?

By now you should understand the theory of JavaScript Promise. Now it’s time to get into practice.

Nowadays, unless you work only with vanilla JavaScript, it’s not very common to create Promises on your own. But as a user of JavaScript frameworks, you’ll work with Promises quite often. Knowing the API of the Promise is of vital importance.

Now let’s get to it.

Consuming Promise

The most elementary Promise operation is then(). The method allows you to get the successful result of the Promise.

dinnerPreparation.then(function (result) {
   console.log(result);
});

If the result isn’t ready yet when you call then(), your script execution will be blocked until the result is available. However, if the result is ready, the execution of the script continuous.

As you see, the .then() method accepts a callback function as its input. JavaScript runs this method once the result of the Promise is ready. The result is passed to the callback as an argument. And guess what? It’s the same object you pass via resolve() function in the Promise executor.

The last thing, .then() returns a new Promise as the output. The result of this Promise depends on the value returned by your callback function you pass as the input. If you don’t return anything, the result will be undefined.

When you process the result of a Promise, you can:

  • consume the result and return nothing
  • extend the result and return it for further processing
  • return another result based on the input

To illustrate this, the second option will look as follows:

let dinnerPreparation = Promise.resolve('Dinner is ready!');

let newDinnerPromise = dinnerPreparation.then(function (result) {
   console.log(result); //Dinner is ready!
   let extendedResult = 'Wash your hands! ' + result;
   return extendedResult;
});

In a moment, you’ll see why returning another Promise is useful. But before that, let’s have a look at …

Handling Promise errors

What if your Promise fails? The callback you pass to .then() won’t be called. You need to register another callback to handle the failure.

You have two options here:

  1. pass the second argument of the then() method to register a failure callback
  2. use the catch() method

The difference, however, is pretty cosmetic.

Handling Promise errors with then()

The then() method has an optional second parameter, which just like the first one is a callback function. However, this callback is called only if the Promise fails. Or in other words, if the executor of the Promise calls its reject() function. The variable passed to the callback is the same as the argument passed to reject().

In practice, it looks like this:

dinnerPreparation.then(function (result) {
   console.log(result);
}, function (error) {
   console.log(error);
});

Let’s compare it with the second option.

Handling Promise errors with catch()

Another approach, which is more popular, is registering the callback via the catch() method you call on the Promise.

dinnerPreparation.catch(function (error) {
   console.log(error);
});

Under the hood, catch() passes its input to the then() method as a second argument (the first one is set to undefined). Actually, the catch() method is just syntax sugar. Yet, JavaScript programmers prefer this approach because of its readability. Especially, if you use it together with …

Promise chaining

Since both then() and catch() methods return a new Promise as their results, you can fluently call these methods as a readable sequence of steps.

let dinnerPreparation = Promise.resolve('Dinner is ready!');

dinnerPreparation.then(function (result) {
   console.log(result); //Dinner is ready!
   let extendedResult = 'Wash your hands! ' + result;
   return extendedResult;
}).then(function (result) {
   console.log(result); //Wash your hands! Dinner is ready!
}).catch(function (error) {
   console.log(error);
});

Be warned:

This naive example only demonstrates how the result of one Promise is passed to another for further processing. In the real world, we use Promise chaining only to composite several asynchronous operations.

What you see in the above example is an antipattern called Promise abuse. We actually should merge callbacks from both then() calls.

Why?

Because there is no real benefit from creating a new Promise in the first call to then() method by returning the value. If you write a chain of then() calls, ideally only the last one should be synchronous and return no value.

Promise chaining – Example of real use

The most common asynchronous operations we do in web apps are backend service calls.

The Promise chaining comes in handy when we want to do two (or more) calls and the second call to a service depends on the result of the first one. In other words, when we have a sequence of asynchronous calls.

Let’s illustrate this with an example.

Imagine a typical e-commerce site where the user can browse products. First, the user types some search criteria. Then, the application needs to fetch the first ten product details from a backend service. Finally, after the response arrives, the application makes a second request to another service to fetch images of these ten products. This is the perfect use case for JavaScript Promise chaining.

In JavaScript it could look like this:

function findProducts(criteria) {
   // returns new Promise
}

function fetchProductImages(products) {
   // returns new Promise
}

findProducts(criteria)
   .then(fetchProductImages)
   .then(function (images) {
       // display images
   }).catch(function (error) {
       // handle all chain errors
   });

Although we have two Promises for asynchronous operations, we don’t run both operations parallel but in a sequence. The possibility of chaining is what makes Promises superior to regular callback methods as the solution for a sequence of asynchronous calls. The structure of the code is flat. There is no pyramid of callbacks as presented at the beginning of the article.

JavaScript Promise chain

We can also define error handling in a single point. If one of Promise fails, the function from the catch() method will take care of the error.

On the other hand, if you want to run asynchronous operations in parallel, JavaScript Promise will also help you.

Combine multiple parallel Promises into one

Sometimes, we have several asynchronous operations which don’t depend directly on each other. However, we want to wait for all of them to complete before further processing because we want to combine their results.

That’s where Promise.all() comes in.

Promise.all() accept multiple Promises as the input. The function returns another Promise which after completion contains an array of results from all Promises passed as arguments.

Let’s return to our example with the e-commerce site. This time, we would like to find products for the search criteria but also fetch some promoted shop offers to display them on top of the found products.

We could code this use case as follows:

function findProducts(criteria) {
   // returns Promise
}

function fetchPromotedOffers() {
   // returns Promise
}

Promise.all([findProducts(criteria), fetchPromotedOffers()])
   .then(function (results) {
       let products = results[0];
       let promotedOffers = results[1];
       // ... process together
   }).catch(function (error) {
       // handle any error
   });

If any of the input Promises fails, the Promise returned from the Promise.all() method will also be failed.

JavaScript Promise.all()

Compete against several Promises

Another useful use case for JavaScript Promise is when you want to run multiple asynchronous operations but process only the fastest result. In other words, you start several operations in parallel, get only the first result that arrives, and ignore all other results.

Let’s get back for the last time to our imagine e-commerce site. To improve user experience and decrease the time users need to wait, we may duplicate our search product backend services. Thanks to Promise.race(), we can call both services at the same time and display only the fastest result to the user.

function findProductsInNode1(criteria) {
   // returns Promise
}

function findProductsInNode1(criteria) {
   // returns Promise
}

Promise.race([findProductsInNode1(criteria), findProductsInNode2(criteria)])
   .then(function (result) {
       // display first result
   }).catch(function (error) {
       // handle any error
   });

The signature of Promise.race() is identical to Promise.all(). The difference between methods is in their behavior and returned results.

JavaScript Promise.race()

Unfortunately, there is one big issue with the Promise.race() function. It returns the first result no matter if it’s successful or failed.

In our example, if the first service returns an error before the second service returns a successful result, we’ll have to display the error to the user. The later response is ignored, even though it’s successful.

I strongly hope this limitation will be handled in the future JavaScript language updates.

Finalizing JavaScript Promise

Last but not least, you may also want to run some code when a Promise is finished regardless of its result. For this use, the API of Promise provides us with the finally() method.

function findProducts(criteria) {
   // returns Promise
}

findProducts.then(function (products) {
   // display products
}).catch(function (error) {
   // display error
}).finally(function () {
   // no matter what,
   // hide loading indicator
});

Technically, you can put the code from the finally() callback into a named function and run it at the end of both then() and catch(). Again, the finally() method is another example of a syntax sugar introduced for improved code readability.

Conclusion

The JavaScript Promise might seem a bit complicated at first but after finishing this article, you should understand how to work with them and when it’s a good idea to use one. The API of the Promise object shouldn’t have any secrets from you.

If you’re interested in how JavaScript handles asynchronous operation under the hood, once again I encourage you to see my article about JS runtime environment. Also, if you find the article useful, please share it with your followers. You can also consider subscribing to my mailing list so you won’t miss future posts about web development.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement

Leave a Reply