Advanced JavaScript Node.js Promises chaining collections

October 10, 2019 0 Comments

Advanced JavaScript Node.js Promises chaining collections

 

 

JavaScript Promise objects are a considerable improvement over basic JavaScript callback functions. Promises provide an object that represents both the state of an asynchronous function and the values returned by it. The state indicates whether the promise is pending, fulfilled by a successfully completed operation, or rejected because of an error or other condition. When a promise is fulfilled its value is determined by the asynchronous operation executed by the promise.

Promises can also be chained together to use the state of settled promises (resolved or rejected) and the promises’ values to control the flow of execution in your program. This is useful when you have a sequence of asynchronous operations to perform that each depend on the results of the previous operation: the next step in the chain isn’t performed until the preceding step is resolved. Chains of promises also include error propagation; errors in a chain of promises need only be handled once.

Promise objects also have static methods that enable you to work with a Promise object that contains a collection of Promise objects. You can iterate over the collection and evaluate both the state of each promise in the collection and the values in each promise element. The Promise object static methods enable you to execute code only when all the promises in a collection have been fulfilled or as soon as one of them is fulfilled.

This post shows you how to do promise chaining and how to work with collections of promises with the static methods. It also shows you how to work around some of the limitations of the Promise object static methods.

If you need an introduction to Promise objects the previous post in this series, Asynchronous JavaScript: Introduction to JavaScript Promises, explains how to create and use Promise objects in place of callbacks.

The posts in this series on Asynchronous JavaScript share a common code base to make moving between them easy. You can use the Git repo branches from previous posts to compare the techniques taught in each step in the series.

Prerequisites

To accomplish the tasks in this post you will need the following:

You should also have a working knowledge of the core elements of JavaScript, including object constructors and anonymous functions. Read the first post in this series, Asynchronous JavaScript: Understanding Callbacks, if you are not familiar with the JavaScript event model.

The companion repository for this post is available on GitHub.

Setting up the project

Clone the project by executing the following command-line instructions in the directory where you would like to create the project root directory:

git clone https://github.com/maciejtreder/asynchronous-javascript.git  
cd asynchronous-javascript  
git checkout step6  
npm install  
cd promises  

Alternately, if you have been following along with this series and have completed the project from Asynchronous JavaScript: Introduction to JavaScript Promises post you can continue with the code you wrote for that post.

Chaining promises

To understand chaining promises together, start with a quick review of how promises are used with the .then and .catch methods. These methods form most of the links in a chain of promises.

The .then method of the Promise object prototype appends handler functions to a Promise object. It contains both an onResolved parameter and an onRejected parameter so you can pass functions to the .then method to handle cases where the promise is successful and those which result in an error (or another condition requiring rejection of the promise). The .then method returns a new Promise object with the results of the relevant handler function. In simpler terms, the .then method enables you to specify actions to take on the value of a promise depending on whether is is fulfilled or rejected.

You can find a more detailed explanation and demonstration of the features of the .then method in the previous post in this series, Asynchronous JavaScript: Introduction to JavaScript Promises. Look for the Getting started with JavaScript Promises section.

You can also see a working example of the .then method in the companion repository for this series. The code in the promises/practical.js file provides an example in which the functions passed in the onResolve and  onRejected parameters are called randomly based on the results of the asynchronous function returning the promise.

You can also use the .catch method of the Promise prototype to handle rejected promises. You can see an example in the promises/catch.js file the companion repository. The code refactors practical.js to use .catch instead of the onRejected argument of the .then method.

If you have code you always need to execute regardless of whether the promise is fulfilled or rejected, you can call it with the .finally method of the Promise prototype. You’ll learn more about the .finally method in this post.

Chains of promises gives you the ability to use the values returned by asynchronous functions to select and control the flow of execution in your program based on the results of each promise in the chain when they become available. You can also use the state of the promise, fulfilled or rejected, to determine the flow of execution. Because promises can be reused you can use multiple instances of the same promise, typically with different arguments, to obtain different results, then use the results of the preceding promise in the chain as an argument in the next one.

Chaining promises is usually done with two methods of the Promise object prototype: .then and .catch. The .then method specifies the action to perform when a promise is fulfilled. The .catch method specifies what to do when promise is rejected. This is usually when an error occurs, but you can also reject a promise explicitly without generating an error (although it is good practice to do so). Because those two methods return Promise objects, you can use .then and .catch on it again and again:

function multiply(a) {  
 console.log(a);  
 if (a > 16)  
 throw `a is higher than 16`;  
  return Promise.resolve(a*a);  
}  
 Promise.resolve(2) // Returns a fulfilled promise with value 2  
 .then(multiply) // console.log(2); Promise fulfilled, returns 4;  
 .then(multiply) // console.log(4); Promise fulfilled, returns 16  
 .then(multiply) // console.log(16); Promise fulfilled, returns 256  
 .then(multiply) // console.log(256); Promise rejected, throws `a is higher than 16`  
  .then(multiply) // not reached  
 .catch(error => {console.log(`Caught: ${error}`); return 3;}) //Catches error ‘a is higher than 16’ and returns 3  
 .then(multiply) // console.log(3); Promise fulfilled, returns 9  
 .then(console.log) //console.log(9); Promise fulfilled, returns void/undefined  

Note that the order of .then and .catch methods can change which methods are called if an error is thrown. This can cause multiple execution paths through your chain of promises, depending on where errors are thrown and caught, as shown in the example above.

Take a look at a real-life example. If you want to check the temperature in the San Francisco area, which is located at about latitude 37 North, longitude 122 West, you could get the current weather data for those coordinates in JSON form. There’s an API to do that at the www.7timer.info website. Try the request with your browser at the following URL:

http://www.7timer.info/bin/api.pl?lon=122&lat=37&product=astro&output=json

You should see results similar to the following (depending on the weather at the moment you make the API call):

{  "product":"astro",  
 "init":"2019092706",  
 "dataseries":[  {  "timepoint":3,  
 "cloudcover":1,  
 "seeing":7,  
 "transparency":3,  
 "lifted_index":2,  
 "rh2m":10,  
 "wind10m":{  "direction":"S",  
 "speed":2  
 },  
 "temp2m":21,  
 "prec_type":"none"  
 },  
 {  "timepoint":6,  
 "cloudcover":1,  
 "seeing":8,  
 "transparency":3,  
 "lifted_index":2,  
 "rh2m":12,  
 "wind10m":{  "direction":"S",  
 "speed":2  
 },  
 "temp2m":16,  
 "prec_type":"none"  
 },  
 ...  
 ]  
}  

In the example above, ellipsis (‟...”) is used to represent a portion of the data redacted for brevity.

As you can see, the results are not conveniently readable if you are just interested in one of the values. The temperature in degrees Celsius is located under the dataseries[n].temp2m key. Extracting just the temperature from this data series can easily produce the JavaScript “pyramid of doom” if you use callbacks. (An example of a code pyramid can be found in Asynchronous JavaScript: Understanding Callbacks in the section Nested callbacks: creating a chain of asynchronous functions.)

That’s the point where you can benefit from chaining promises together.

To work with next code example, you will need to install the node-fetch library from the npm repository. This library provides the fetch functionality (exactly the same as in modern browsers) to the Node.js environment.

Execute the following command-line instruction in the asynchronous-javascript directory:

In the asynchronous-javascript/promises directory, create a new file, chaining.js. Insert the  following JavaScript code:

const fetch = require('node-fetch');  
  
const lat = 37;  
const lon = 122;  
   
fetch(`http://www.7timer.info/bin/api.pl?lon=${lon}&lat=${lat}&product=astro&output=json`)  
 .then(response => response.json())  
 .then(response => response.dataseries[0].temp2m)  
 .then(temperature => console.log(`The temperature reported by the 7timer is: ${temperature} C`));  

The series of .then method calls is a chain. The first link takes the Response object from the www.7timer.info API call and executes the Body.json() method on it. The Body.json method reads the Response stream and returns a promise with a JSON object containing the data from the Response stream as its value. The next .then method finds a specific data element in the JSON object and returns a promise with the retrieved data element as its value. A third .then method is applied to this promise chain to take the temperature value and send it to standard output.

Run the code by executing the following command-line instruction in the promises directory:

The script should produce output similar to the following, depending on the temperature in San Francisco at the moment you run the code:

The temperature reported by the 7timer is: 21 C  

If you want to catch up your code to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project directory:

git clone https://github.com/maciejtreder/asynchronous-javascript.git  
cd asynchronous-javascript  
git checkout step7  
npm install  
cd promises  

A group of promises consists of an array of Promise objects. You can perform the same functions on a group of Promises as on any other type of array:

const group = [  
 fetch(`url1.com/path`),  
 fetch(`url2.com/path`),  
 fetch(`url3.com/path`)  
]  
group.push(fetch(`url4.com/path`));  

Often your code will depend on multiple promises. For example, you may want to display data from multiple data sources, such as REST calls or files. The Promise object includes some useful static functions which you can use to work with groups of promises:

Promise.all

Promise.all is a static function which combines an array of promises into one promise. The consolidated promise state is determined when all the promises in the array have been settled (resolved or rejected). If any promise in a collection is rejected the result of the Promise.all method is a rejected promise with the rejection reason taken from the first failing promise. The result of a Promise.all method evaluation can only be a fulfilled promise when all the promises in the array are settled as fulfilled. In other words, it is an “all” or nothing deal.

Create a new file called all.js and insert the following code:

const fetch = require('node-fetch');  
 const lat = 37;  
const lon = 122;  
 const sevenTimerTemp = fetch(`http://www.7timer.info/bin/api.pl?lon=${lon}&lat=${lat}&product=astro&output=json`)  
.then(response => response.json())  
.then(response => response.dataseries[0].temp2m);  
 const fccWeatherTemp =  
fetch(`https://fcc-weather-api.glitch.me/api/current?lat=${lat}&lon=${lon}`)  
.then(response => response.json())  
.then(response => response.main.temp);  
 Promise.all([sevenTimerTemp, fccWeatherTemp])  
.then(responses => {  
 let sum = 0;  
 let count = 0;  
 responses.forEach(response => {  
 sum += response;  
 count++;  
 });  
 return sum / count;  
})  
.then(average => console.log(`Average of reported temperatures is: ${average} C`))  
.catch(() => console.log(`One of the APIs returned an error`));  

Run the code by executing following command:

The output should be similar to the following:

Average of reported temperatures is: 21.615000000000002 C  

There is a possibility that you will run into a REST call rejection in one of the APIs. This is particularly true of the fcc-weather-api.glitch.me API, which is shared among many users. When either of the API calls fails the following would be displayed in the console window:

One of the APIs returned an error  

That’s the pitfall of the Promise.all method; it immediately rejects if one of the provided promises rejects. It doesn’t even wait for all the promises to be settled.

There is a simple workaround for that drawback: chain all your promises with a custom .catch method.

Add the following code at the bottom of the all.js file:

Promise.all([  
 sevenTimerTemp.catch(() => -100),  fccWeatherTemp.catch(() => -100)  
])  
.then(responses => {  
 let sum = 0;  let count = 0;  
 responses.filter(response => response ! -100).forEach(response => {  
 sum += response;  
 count++;  
 });  
 return sum / count;  
})  
.then(average => console.log(`Average of reported temperatures is: ${average} C`));  

This code takes an alternate approach to the first use of Promise.all. Each of the constants passed to the Promise.all method in the array is modified with the .catch method. If the promise state associated with the constant is rejected because of an invalid HTTP response from the associated API, the .catch method will set the value of the associated promise to -100.  In the first .then handler method attached to the Promise object returned by the Promise.all method the code skips that unrealistic value when calculating the average temperature.

Promise.allSettled

There is also another way of dealing with the case of failing promises: the Promise.allSettled method, which is currently in the proposal state. Because this functionality is not supported by most browsers or by Node.js in versions below 12.9, it’s a good practice to use the promise.allSettled shim available in the npm repository.

Install it with the following command:

Create a new file called allSettled.js in the promises directory and insert the following JavaScript code:

const fetch = require('node-fetch');  
const allSettled = require('promise.allsettled');  
allSettled.shim();  
 const lat = 37;  
const lon = 122;  
 const sevenTimerTemp = fetch(`http://www.7timer.info/bin/api.pl?lon=${lon}&lat=${lat}&product=astro&output=json`)  
.then(response => response.json())  
.then(response => response.dataseries[0].temp2m);  
 const fccWeatherTemp =  
fetch(`https://fcc-weather-api.glitch.me/api/current?lat=${lat}&lon=${lon}`)  
.then(response => response.json())  
.then(response => response.main.temp);  
 Promise.allSettled([sevenTimerTemp, fccWeatherTemp])  
.then(responses => {  
 let sum = 0;  
 let count = 0;  
 responses.filter(response => response.status = 'fulfilled').forEach(response => {  
 sum += response.value;  
 count++;  
 });  
 return sum / count;  
})  
.then(average => console.log(`Average of reported temperatures is: ${average} C`));  

At the beginning of the file, add two lines of code to change the Promise object prototype:

const allSettled = require('promise.allsettled');  
allSettled.shim();  

This code enables you to use the allSettled method on the Promise class if it is not already enabled. If you are running Node.js version 12.9 or higher (and you probably are), then the .shim method call results in a no-op and the native version of the static method is used.

Run the code and check that the output is similar to the following:

Average of reported temperatures is: 19.72  

The script waits for all promises provided to the allSettled method to be settled. The code in the .then method call only includes the values from promises in the array that are fulfilled. If there is a REST call failure in any of the API calls, the status of the associated promise will be rejected and its value skipped when calculating the average temperature.

The promise returned by the allSettled method is always fulfilled: it never returns a status of rejected. The value of the resolved promise is an array containing information about child promises statuses and values.

Promise.race

A last—but not least—case for using promises: what if you want to get information as fast as possible using alternative data sources? The Promise.race method is intended for that purpose.

Create a file called race.js and insert the following code:

const fetch = require('node-fetch');  
 const lat = 37;  
const lon = 122;  
 const sevenTimerTemp = fetch(`http://www.7timer.info/bin/api.pl?lon=${lon}&lat=${lat}&product=astro&output=json`)  
.then(response => response.json())  
.then(response => response.dataseries[0].temp2m);  
 const fccWeatherTemp =  
fetch(`https://fcc-weather-api.glitch.me/api/current?lat=${lat}&lon=${lon}`)  
.then(response => response.json())  
.then(response => response.main.temp);  
 Promise.race([sevenTimerTemp, fccWeatherTemp])  
.then(temperature => console.log(`Temperature reported by the fastest station is: ${temperature} C`));  

Run the code and review the output. You will see something similar to the following, or a runtime error:

Temperature reported by the fastest station is: 24.44 C  

You’ll see an error if one of the REST calls fails, which will result in the associated promise being rejected. What happens when one of the promises provided to the race method rejects? The race method will behave in a similar fashion to Promise.all: it will reject immediately.

What if you want to get results from the first positive (fulfilled) promise? Similar to the Promise.all method, chaining will do the trick.

Add the following code to the bottom of the race.js file:

function getWatchdog(timeout) {  
 return new Promise((resolve, reject) => setTimeout(() => reject('Timeout !'), timeout * 1000));  
}  
  
Promise.race([  
 sevenTimerTemp.catch(() => new Promise((resolve) => {/*noop*/})),  fccWeatherTemp.catch(() => new Promise((resolve) => {/*noop*/})),  
 getWatchdog(5)  
])  
 .then(temperature => console.log(`Temperature reported by the fastest station is: ${temperature} C`))  
 .catch(() => console.log('Unfortunately no API responded within the given time-out interval'));  

The code you just inserted replicates the functionality of the original code in a more resilient fashion. Each of the promises associated with the API calls have been chained with a .catch method which returns a never-resolving promise that will be returned in the event of an exception. This technique guarantees that the Promise.race promise won’t resolve until one of the promises provided as parameter does.

What if all of the child promises reject? Will you wait till the end of the world for the Promise.race call to complete? No. The getWatchdog function, which returns a promise, resolves after a given amount of time.

Re-run race.js and check the output for something similar to the following:

Temperature reported by the fastest station is: 28.23 C  
Temperature reported by the fastest station is: 28.23 C  

The first line out output is from the original code. The second is from the code added immediately above.

When you run race.js you are likely to see a slight delay before the console window prompt returns from code execution. Even though one of the weather API calls returns successfully with a value—winning the race—the asynchronous call to setTimeout in the getWatchDog function executes after the Promise.race method evaluation is complete.

Now, your script is “reject agnostic”!        

If you want to catch up to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project directory:

git clone https://github.com/maciejtreder/asynchronous-javascript.git  
cd asynchronous-javascript  
git checkout step8  
npm install  
cd promises  

This post explained the concept of chaining JavaScript promises together and showed you how to use the Promise object prototype methods .then, .catch, and .finally to manipulate the results of the asynchronous operations in a chain of promises, and it explained how to manage the flow of control in your code with promise chaining. It also explained and demonstrated how to use the Promise object static methods to work with iterable collections of promises. JavaScript promises are a considerable improvement over basic callbacks and learning to use these advanced features of the Promise object can help you in many programming situations.

Additional Resources

Promise – Mozilla Developer Network (MDN) documentation of the  Promise object constructor

Using Promises – Mozilla Developer Network (MDN) documentation for using Promises in code, including the prototype methods .then, ,catch and .finally.

Promises, async/await – The javascript.info website provides a nice section on asynchronous techniques in JavaScript, including callbacks, Promises, and async/await. It’s attractively put together and includes well-executed diagrams.

There are many other blog posts and articles on the web about JavaScript Promises and a lot of them are inaccurate, incomplete, or confusing. Callback functions are always a tough topic to write about and the timing considerations in Promises make them even more difficult. If you have an issue with any part of this introduction to Promises, please bring it to the attention of the author so we can fix it.

Maciej Treder is a Senior Software Development Engineer at Akamai Technologies. He is also an international conference speaker and the author of @ng-toolkit, an open-source toolkit for building Angular progressive web apps (PWAs), serverless apps, and Angular Universal apps. Check out the repo to learn more about the toolkit, contribute, and support the project. You can learn more about the author at www.maciejtreder.com. You can also contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.

Authors


Tag cloud