Asynchronous JavaScript: Refactoring Callbacks to Promises in Node.js
Time to read: 6 minutes
If you know how the event loop mechanism works in JavaScript you know how it enables you to work with asynchronous events. You might also know how to refactor your code into separate functions to reduce the amount of nesting associated with a sequence of callback functions. If you work with web APIs you are probably familiar with the JavaScript request library, which is used to perform HTTP calls.
You can put these skills together to retrieve remote data through an HTTP request and work with the call response asynchronously. But in doing so you’ll probably notice that code has at least one pitfall: the logical order of declared functions is reversed. That makes the code hard to read and maintain.
With the node-fetch JavaScript library and Promise objects you can reduce nesting, known as the callback “Pyramid of Doom”, and organize your code in a more readable and logical order.
While this post focuses on how to improve the modularity and flow of your code with these tools, the previous posts in the series explain Promise objects in more detail:
Asynchronous JavaScript: Introduction to JavaScript Promises
Asynchronous JavaScript: Advanced Promises with Node.js
If you need to brush up on callbacks you can learn about them in the first post in this series, Asynchronous JavaScript: Understanding Callbacks.
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 if you are not familiar with the JavaScript event model.
There is a companion repository for this post, available on GitHub.
Setting up the project
If you have completed the project from the Asynchronous JavaScript: Advanced Promises post you can continue with the code you wrote for that post. If you are familiar with the event loop mechanism, or want to start fresh, you can get the code from GitHub.
Clone the project by executing the following command-line instructions in the directory where you would like to create the project root directory:
Understanding the case study project
The case study project for this post determines the best movie by Quentin Tarantino based on review scores. To accomplish this, the code will retrieve data from REST API mockups on GitHub.io. It achieves the same goal with Promise objects as the case study in the Asynchronous Javascript: Organize Callbacks for Readability and Reusability post did with callbacks.
You can simulate the process manually by performing GET calls with your browser:
- Navigate to https://maciejtreder.github.io/asynchronous-javascript/directors/. You should see the list of JSON objects representing directors. Quentin Tarantino has
id
2. - Navigate to https://maciejtreder.github.io/asynchronous-javascript/directors/2/movies/ to see the list of Tarantino films.
- See the reviews for Inglourious Basterds by navigating to https://maciejtreder.github.io/asynchronous-javascript/movies/5/reviews.
- Add up all the scores and divide by the number of scores to get the average review score.
In addition to learning how to code a project like this, you’ll see a number of advantages to using promises for retrieving and manipulating API data:
- promises can be chained together
- promises can include error rejection events
- errors in a chain of promises need only be handled once
Retrieving REST API data using node-fetch and promises
Armed with knowledge about promises, you can write a more readable program to retrieve the API data and determine the movie with the highest review score. To develop a deeper understanding of this refactoring compare the code in this project with the equivalent code in the organizedCallbacks.js file from the companion repository.
Start refactoring from the first REST API call in asynchronous-javascript/asyncCallback.js. The first step is to use a Promise object instead of a callback to get a list of directors from the API.
In the asynchronous-javascript/promises directory, create a new file named fetch.js and insert the following JavaScript code into it:
Run the code:
In the output you should see list of directors in JSON format:
Yay. That works. The output is the same as from the callback in asyncCallback.js.
As you probably know by now, you can chain multiple promises together and return the manipulated output. You can do that to perform multiple API calls in a series of callback functions using the Promise object prototype .then
method.
Replace the code in the fetch.js file with following:
Run the code. You should see a JSON format list of movies directed by Quentin Tarantino.
What you are doing here is manipulating the response from the first API call: you are looking for the Quentin Tarantino id
value and using it with the next fetch
call, then passing the result forward to another promise:
With response.json()
you are retrieving the JSON body from the API response, which is a list of movies ready to pass to the next promise in the chain. The last line sends the list of movies to the console window.
Now that you have a list of Quentin Tarantino’s movies you can make multiple API calls to retrieve the score from each of the reviews. Then you can collect them into an array of Promise
objects and use the Promise.all
static method to return a single promise containing all the reviews.
Insert the following code into the fetch.js file immediately before the .then(console.log)
line:
In the console window output you should see a list of nested objects containing the reviews for each movie:
Now that you have all the review responses in hand, you can calculate the average score and determine which movie is the best one.
Replace the last line of fetchMultiple.js (the .then(console.log)
statement) with the following code:
Run the fetch.js code. You should see output indicating which movie the reviews favor:
The code in fetch.js is now functionally equivalent to the code in both nestedCallback.js and organizedCallbacks.js. It’s more maintainable and easier to debug than the nested callback structure and it’s more readable than the organizedCallbacks.js because the code is organized from top to bottom in the order of execution.
Your application workflow is described by the following diagram. Every request to an API returns a Promise object, which incorporates a call to another request. When the request is completed and the promise resolved (or rejected), the event loop pushes the next promise to the execution stack.
Adding an interactive element to the command line output
The chain of asynchronous promise operations you’ve created thus far work great for retrieving data, but sometimes APIs or the internet can be slow to respond. It would be helpful to let the user know your JavaScript program is working while they’re waiting.
Fortunately, there’s a nice npm package to do that for command-line applications, the ora library. Install it with following command:
At the very beginning of the file initialize the loader:
And replace last then
with the following lines, which will cause loader to stop and display the results:
Handling errors in promises using the .catch method
In addition to enhancing the command line experience for your application’s users, your latest modifications also added error handling by including a .catch
method call. If any of the promises are rejected, either because of an invalid API response or because of an execution error, the .catch
method will handle the error and call the ora spinner.fail
method. The default behavior for this method is to display a red “x”.
You can easily try this by replacing the tarantinoId
or movie.id
values in the API calls with “fail”, or some other invalid argument. You’ll see that errors that occur early in the chain of promises are handled by the .catch
method, even though it’s the last method call in the chain.
If you haven’t been coding along and want to catch up to this step, execute the following commands to clone the project from the companion repository:
Summary
You use JavaScript Promise objects to refactor JavaScript code written with basic callbacks. Promises enable you to implement asynchronous functionality in a way that’s more robust, reusable, maintainable, and easier to read. This post demonstrates how to refactor an existing program which makes multiple API calls with traditional callbacks into a structure based on promise chaining using the Promise object prototype .then
method. The project also includes error handling with the Promise object prototype .catch
method and it demonstrates how to incorporate external modules into the asynchronous functions called in the promise chain.
Additional resources
Complex async code made easier – developer.google.com presents a similar case study and provides a more extensive explanation of error handling and additional advanced topics.
Creating a Promise around an old callback API – developer.mozilla.org provides instructions for wrapping an API that doesn’t return a Promise object with a Promise so it can be used in places, like a chain of promises, where a Promise object would be required. It’s worth noting the the ubiquitous setTimeout
function is an example.
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 https://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.