If you’ve written some asynchronous JavaScript code before, then you already have an idea about using callbacks and the issues with them. One major issue with using callbacks is the possibility of running into a callback hell.

In ES6, JavaScript Promises were added to the language spec, bringing about an entirely new shift in how asynchronous code is written, and also mitigating the issue of running into a callback hell. If you are using ES6 syntax in your code, you may already be familiar with Promises.

In this guide, you will see some practical ways to improve asynchronous programming in JavaScript using promises.

This guide is not in anyway an introduction to JavaScript promises. Some prior knowledge of Promises is required to read this guide.

Creating promises

A JavaScript promise can be created using the Promise constructor. The constructor function takes an executor function as its argument which is immediately executed to create the promise.

The executor in turn, can take two callback functions as its arguments, that can be invoked within the executor function in order to settle the promise, namely:

  • resolve for fulfilling the promise with a value
  • reject for rejecting the promise with a reason (usually an error)

Here is a very simple JavaScript promise that is settled after loading an image:

Here, you can see that the returned promise will be fulfilled with the img Image object if the image was loaded successfully. Otherwise, it will be rejected with an error indicating that the image could not be loaded.

Settled promises

Often times, you just want to create a promise that is already settled (either fulfilled with a value or rejected with a reason). For cases like this, the Promise.resolve() and Promise.reject() methods come in handy. Here is a simple example:

There could also be times when you are not sure if a value is a promise or not. For cases like this, you can use Promise.resolve() to create a fulfilled promise with the value and then work with the returned promise. Here is an example:

By leveraging settled promises, you can modify the loadPhoto() promise factory from before like this:

In this code snippet, two obvious changes have been made:

  • First, the promise executor function is defined with only one argument instead of two.
// BEFORE
new Promise((resolve, reject) => {...})
// NOW
new Promise(resolve => {...})
  • Second, in the ‘error’ event listener, the promise is fulfilled by a settled promise (rejected with an error), instead of the former variant in which the promise is directly rejected with the error.
// BEFORE
reject(new Error('Image could not be loaded.'));
// NOW
const rejection = Promise.reject(new Error('Image could not be loaded.'));
resolve(rejection);

It is important to note that the two code snippets still work in the same way — which can be attributed to the mechanism for creating intermediate promises during handling of the settled promise.

Handling promises

A settled promise can be handled by chaining callbacks to the then(), catch() or finally() methods of the promise.

The following code snippet demonstrates how you can handle the settled promise returned from the loadPhoto() function we created earlier:

Since the returned promise is fulfilled with an Image object, you can do something more useful in the .then() handler callback, such as adding attributes to the image and appending the image to the document.

Here is an improved code snippet that does just that:

The following CodePen shows a demo of this example:

Dumb then handlers

The .then() method can take up to two handler functions as its arguments: fulfillment handler and rejection handler respectively.

However, if any of these two arguments are not a function, .then() replaces that argument with a function and continues with the normal execution flow. It becomes important to know what kind of function the argument is replaced with. Here is what it is:

  • If the fulfillment handler argument is not a function, it is replaced with an Identity Function. An identity function is a function that simply returns the argument it receives
  • If the rejection handler argument is not a function, it is replaced with a Thrower Function. A thrower function is a function that simply throws the error or value it receives as its argument

If you observe carefully, you will notice that neither the identity function nor the thrower function alters the normal execution flow of the promise sequence.

They simply have the same effect as omitting that particular .then() call in the promise chain. For this reason, I usually refer to these handler arguments as “dumb handlers”.

Then handlers return promises

One important thing to understand about the .then() promise method is that it always returns a promise.

Here is a breakdown of how .then() returns a promise based on what is returned from the handler function is passed to it:

Timing with promises

Delaying execution

Promises can be very useful for timing applications. Some programming languages like PHP have a sleep() function that can be used to delay the execution of an operation until after the sleep time.

While a sleep() function does not exist as part of the JavaScript spec, the global setTimeout() and setInterval() functions are commonly used for executing time-based operations.

Here is how the sleep() function can be simulated using promises in JavaScript. However, in this version of the sleep() function, the halt time will be in milliseconds instead of seconds:

Here is a slightly expanded and annotated version of the sleep(ms) function:

The sleep(ms) function can even be improved further to become a self-contained delay function that executes a callback function after the specified sleep time.

Here is what using the sleep(ms) function could look like:

Measuring execution time

What if you are interested in knowing how long it took for an asynchronous operation to be completed? This is usually the case when benchmarking the performance of some form of implementation or functionality.

Here is a simple implementation that leverages a JavaScript promise to compute the execution time for an asynchronous operation.

In this implementation, performance.now() is used instead of Date.now() for getting the timestamp with a higher resolution. For non-browser environments where the performance object does not exist, you can fallback on usingDate.now() or other host implementations.

Here is how the timing() function could be used to log the execution time of an asynchronous operation on the console:

Sequential execution with promises

With JavaScript promises, you can execute asynchronous operations in sequence. This is usually the case when a later asynchronous operation depends on the execution of a former asynchronous operation, or when the result of a former asynchronous operation is required for a later operation.

Executing asynchronous operations in sequence usually involves chaining one or more .then() and .catch() handlers to a promise. When a promise is rejected in the chain, it is handled by the rejection handler defined in the next .then() handler in the chain and then execution continues down the chain.

However, if no rejection handler has been defined in the next .then() handler in the chain, the promise rejection is cascaded down the chain until it reaches the first .catch() handler.

Case study: photos collection

Let’s say you are building a photo gallery application and you want to be able to fetch photos from an online photo repository and filter the photos by format, aspect-ratio, dimension ranges, etc. Here are some possible functions you could have in your application:

The fetchPhotos() function fetches a collection of photos from the Picsum Photos API using the global fetch() function provided by the Fetch API, and returns a promise that is fulfilled with a collection of photos.

Here is what the collection returned from the Picsum Photos API looks like:

The filter functions accept a collection of photos as an argument and filter the collection in some manner.

  • jpegOnly() — filters a photo collection and returns a sub-collection of only JPEG images
  • squareOnly() — filters a photo collection and returns a sub-collection of only photos with a square aspect-ratio
  • smallerThan() — function is a higher-order function that takes a dimension and returns a photos filter function that returns a sub-collection of photos that have their maximum dimensions below the specified dimension threshold

Let’s say we want to execute this sequence of operations:

  1. fetch the photos collection
  2. filter the collection leaving only JPEG photos
  3. filter the collection leaving only photos with square aspect-ratio
  4. filter the collection leaving only photos smaller than 2500px
  5. extract the photos count and URLs from the collection
  6. log the final output on the console
  7. log error to the console if an error occurred at any point in the sequence

The following code snippet shows how we can chain the execution of these operations in a promise sequence:

The resulting output that is logged to the console looks like this:

Parallel execution with promises

With JavaScript promises, you can execute multiple independent asynchronous operations in batch or parallel using the Promise.all() method.

Promise.all() accepts an iterable of promises as its argument and returns a promise that is fulfilled when all the promises in the iterable are fulfilled or is rejected when one of the promises in the iterable is rejected.

If the returned promise fulfills, it is fulfilled with an array of all the values from the fulfilled promises in the iterable (in the same order). However, if it rejects, it is rejected because of the first promise in the iterable that rejected.

Case study: current temperatures

Let’s say you are building a weather application that allows users to see the current temperatures of a list of cities they’ve selected.

The following code snippet demonstrates how to fetch the current temperatures of the selected cities in parallel with Promise.all(). The OpenWeatherMap API service will be used to fetch the weather data:

That was one hell of a code snippet. The important parts to look out for are the fetchTempForCity() and fetchTempForCities() functions.

  • fetchTempForCity() — accepts a single city as its argument and returns a promise that is fulfilled with the current temperature of the given city (in °C) by calling the OpenWeatherMap API service. The returned promise is fulfilled with an array of the format: [city, temperature]
  • fetchTempForCities() — accepts an array of cities and fetches the current temperature of each city by leveraging Array.prototype.map() to call the fetchTempForCity() function on each city. Promise.all() is used to run the requests in parallel and accumulate their data into a single array, which in turn is reduced to an object using Array.prototype.reduce()

Running the above code snippet will log an object that looks like the following on the console:

Rejection handling

It is important to note — if any of the fetch temperature promises passed into Promise.all() is rejected with a reason, the entire promise batch will be rejected immediately with that same reason. That is, if at least 1 out of the 12 fetch temperature promises is rejected for some reason, the entire promise batch will be rejected and hence, no current temperatures.

The scenario described above is usually not the desired behavior — a failed temperature fetch should not cause the results of the successful fetches in the batch to be discarded.

A simple workaround for this will be to attach a .catch() handler to the fetchTempForCity promise, causing it to fulfill the promise with a null temperature value in cases of rejection.

Here is what this will look like:

With that little change to the fetchTempForCity() function, there is now a very high guarantee that the returned promise will never be rejected in cases where the request fails or something goes wrong, rather it will be fulfilled with an array of the format: [city, null]

With this change, it becomes possible to further improve the code to be able to schedule retries for failed temperature fetches.

The following code snippet includes some additions that can be made to the previous code to make this possible.

In this code snippet, the TEMPS object is used to hold the updated temperatures of the listed cities. The MAX_TEMP_FETCH_RETRIES constant is an integer that limits the number of retries that can be done for failed fetches, which is five (5) in this case.

The fetchTemperatures() function receives an array of city names and the number of retries so far as its arguments. It calls fetchTempForCities() to fetch the current temperatures for the cities passed to it and also updates the TEMPS object with the temperatures.

For failed fetches, it schedules a call again to itself after waiting for 5 seconds and increments the retries count by 1. The retries are done for as many times as possible, provided the set maximum has not be exceeded — which is 5 in this case.

Racing with promises

With JavaScript Promises, you can race multiple independent asynchronous operations using the Promise.race() method. Promise.race() accepts an iterable of promises as its argument and returns a promise that is fulfilled or rejected in the same way as the first settled promise in the iterable.

If the first settled promise in the iterable is fulfilled with a value, the race promise is fulfilled with the same value. However, if it is rejected, the race promise will be rejected with the same reason. If multiple promises are fulfilled or rejected at the same time, then the first promise will be used based on the order of the promises in the iterable.

If the iterable passed to Promise.race() is empty, then the race promise remains pending forever and is never settled.

Case study: timeout response

Let’s say you are building an API endpoint that does some asynchronous operation like reading from a file or querying a database, and you want to guarantee that you get a response in 5 seconds — otherwise the request should fail with a HTTP 504 Gateway Timeout response.

The following code snippet demonstrates how Promise.race() can be used to achieve this, assuming we are building the API using the Express framework for Node.js.

In this code snippet, a very minimalistic Express application has been set up with a single route — GET /random for returning a randomly generated integer in the range of 0–100 (both inclusive), while also returning the execution time.

Promise.race() is used to wait for the first of two promises:

  • an execute promise that performs some seemingly time-consuming asynchronous operation and gets settled after 1s — 10s
  • a requestTimer promise that does nothing and gets settled after the set TIMEOUT_SECONDS seconds, which is 5 seconds in this case.

So here is what happens, whichever of these two promises that get settled first will determine the final response from the endpoint — Promise.race() will make sure of that.

A similar technique can also be used when handling fetch events in service workers to detect slow networks.

Conclusion

JavaScript promises can drastically change the way you go about writing asynchronous programs, making your codes more succinct and clearer in regards to the desired intent.

In this guide, you’ve seen several ways promises can be used in asynchronous programs like executing operations in sequence, in parallel and even racing them.

More recent versions of the ECMAScript specification even provide syntax additions to the language such as async functions, await keyword and even async iterators — making it easier and cleaner to work with JavaScript promises.

Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Try it for free.

Clap & follow

If you found this article insightful, feel free to give some rounds of applause if you don’t mind — as that will help other people to easily find it on Medium.

You can also follow me on Medium (Glad Chinda) for more insightful articles you may find helpful. You can also follow me on Twitter (@gladchinda).

Enjoy coding…


Improve async programming with JavaScript promises was originally published in LogRocket on Medium, where people are continuing the conversation by highlighting and responding to this story.