Asynchronous JavaScript: Advanced Promises with Node.js
Time to read: 11 minutes
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:
- Node.js and npm (The Node.js installation will also install npm.)
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:
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:
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):
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:
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:
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:
Working with groups of promises using the static methods
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:
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:
Run the code by executing following command:
The output should be similar to the following:
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:
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:
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:
At the beginning of the file, add two lines of code to change the Promise
object prototype:
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:
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:
Run the code and review the output. You will see something similar to the following, or a runtime error:
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:
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:
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:
Summary
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.