Editor’s note: This article was last updated by Pascal Akunne on 26 April 2024 to cover the Promise.all()
and Promise.race()
methods, update the process of consuming promises to cover the finally()
method when a promise is fulfilled or rejected, and cover limitations to working with promises, and situations where you’d use a process like async/await
instead.
In Node.js applications, it’s not unusual to see a large number of nested callback functions being used to accomplish several activities. This is commonly referred to as callback hell, as it can make the code extremely complicated and disorganized.
Fortunately, there’s a JavaScript solution called promises that solves the callback hell problem. This article will provide an overview of JavaScript promises and demonstrate how to use promises in Node.js with the promisfy()
function.
In order to follow along, you should have the following:
A promise is essentially an improvement of callbacks that manage all asynchronous data activities. A JavaScript promise represents an activity that will either be completed or declined. If the promise is fulfilled, it is resolved; otherwise, it is rejected. Promises, unlike typical callbacks, may be chained.
JavaScript promises have three states: pending, resolved, and rejected. The pending state is the initial state that occurs when a promise is called. While a promise is pending, the calling function continues to run until the promise is completed, returning whatever data was requested to the calling function.
When a promise is completed, it ends in either the resolved state or the rejected state. The resolved state indicates that the promise was successful and that the desired data was passed to the .then()
method.
The rejected state indicates that a promise was denied, and the error is passed to the .catch()
method.
Promises are generally created by calling a Promise
constructor, which accepts a single callback function as an argument. The callback function, also known as the executor function, is executed immediately after a promise is created.
The executor function accepts two callback functions as arguments, resolve
and reject
, which are referred to as function references. The resolve()
and reject()
functions each accepts one argument, which could be a string, integer, Boolean, object, or array.
To better understand how to create a custom promise, let’s look at the following script.js
file:
function getSumNum(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a + b; if(sum <= 5){ resolve("Let's go!!") } else { reject(new Error('Oops!.. Number must be less than 5')) } }) return customPromise }
Here, we define the function getSumNum()
to compute the sum of two integers, a
and b
. Within the function, we use the promise constructor, new Promise()
, to generate a new promise. Next, we compute the sum
of a
and b
. The resolve
callback is executed if the sum
is less than or equal to 5
. Otherwise, the reject
callback is called.
The new promise is passed to the customPromise
variable, which is then returned. In the example above, we return a string, but it could also be an object or an array.
Now that we understand how a promise is created, let’s review how it is consumed.
In application development, it’s much more common to consume promises than it is to create promises.
For example, when we request data from a server via an API that returns a promise, we utilize the then()
and catch()
methods to consume whatever data is delivered:
promise.then(data => { console.log(data) }) .catch(err => { console.log(err) })
In the above code, the then()
method is executed when the promise is fulfilled by the resolve()
callback. The catch()
callback is called if the promise fails, passing the error of reject()
.
Now, let’s consume the promise we created previously:
function getSumNum(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a + b; if(sum <= 5){ resolve("Let's go!!") } else { reject(new Error('Oops!.. Number must be less than 5')) } }) return customPromise } // consuming the promise getSumNum(1, 3).then(data => { console.log(data) }) .catch(err => { console.log(err) })
The sum of one and three is less than five, so the resolve()
callback is run. This, in turn, executes the then()
method. If we change the parameters to result in a sum greater than five, the reject()
callback will be run and an error will be thrown using the catch()
method.
Now, let’s run the following command and then check the console:
node script.js
In cases where we do not care about the resolve or reject result but want to execute something after a Promise
call, we will use the finally()
method. The finally()
method is useful when we want to perform some action once the promise is resolved or rejected. This allows us to carry out some clean-up operations or other tasks that should be executed regardless of the Promise
outcome.
Let’s update the getSumNum()
function:
// consuming the promise getSumNum(1, 3).then(data => { console.log(data) }) .catch(err => { console.log(err) }) .finally(() => { console.log("Code has been executed") })
In the code above, whether the promise is fulfilled or rejected, the finally()
method will execute. The finally()
method does not take an argument because we do not care what the result might be.
Run the command to see the outcome:
node script.js
Promises can be used to execute a series of asynchronous tasks in sequential order. Chaining multiple then()
Promise
outcomes helps avoid the need to code complicated nested functions (which can result in callback hell).
To demonstrate chaining promises, let’s utilize the previous code with a few modifications:
let value; function getSumNum(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a + b; if(sum < 5){ resolve(sum) } else { reject(new Error('Oops!.. Number must be less than 5')) } }) return customPromise } getSumNum(1, 3) .then(data => { console.log("initial data: " + data) value = data + 1 // modifying the returned data return value }) .then(newData => { console.log("modified data: " + newData) }) .catch(err => { console.log(err) })
Here, we see the result is passed through a chain of then()
methods. We begin by declaring an empty variable called value
. This time, instead of resolving a string, we pass the sum
value. When the initial promise object resolves, the then()
function is invoked to log the initial data to the console before modifying the data by adding 1
and then assigning the resulting sum to the value
variable. The value
variable is passed to the next then()
method, where the data is logged to the console.
Now, let’s run the following command:
node script.js
Here’s the output:
initial data: 4 modified data: 5
Promise.all()
methodThe Promise.all()
method handles several promises at once. It accepts an array of promises as an argument and returns a single promise that resolves when all of the promises are fulfilled or rejects when any of the promises are rejected, with the first rejection reason specified. It is useful when we want to do multiple, related asynchronous actions that the whole code relies on to function properly before the code execution can continue.
Consider the following example:
function getSum(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a + b; if(sum >= 5){ resolve("Sum resolved!") } else { reject(new Error('Oops!.. Number must be greater than 5')) } }) return customPromise } function getSub(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a - b; if(sum < 5){ resolve("Subtraction resolved!") } else { reject(new Error('Oops!.. Number must be less than 5')) } }) return customPromise } function getMultiple(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a * b; if(sum < 5){ resolve("Multiplication resolved!") } else { reject(new Error('Oops!.. Number must be less than 5')) } }) return customPromise } let a = 4, b = 1 Promise.all([getSum(a, b), getSub(a, b), getMultiple(a, b)]) .then(values => { console.log(values); // Output: ["Sum resolved", "Subtraction resolved", "Multiplicattion resolved"] }) .catch(error => { console.error(error); });
Run the code and see the output:
node script.js
Promise.race()
methodLike Promise.all()
, Promise.race()
takes in an array of promises as input and returns a single promise. The difference is that the returned promise is the first promise that settles. This means that if a promise resolves first, Promise.race()
resolves with the value of that promise. If a promise is rejected first, Promise.race()
rejects with the value of that promise:
function getSum(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a + b; setTimeout(() => { if(sum >= 5){ resolve("Sum resolved!") } else { reject(new Error('Oops!.. Summation error: Number must be greater than 5')) } }, 5000) }) return customPromise } function getSub(a, b) { const customPromise = new Promise((resolve, reject) => { const sum = a - b; setTimeout(() => { if(sum < 5){ resolve("Subtraction resolved!") } else { reject(new Error('Oops!.. Subtraction error: Number must be less than 5')) } }, 3000) }) return customPromise } let a = 4, b = 1 Promise.race([getSum(a, b), getSub(a, b)]) .then(result => { console.log(result); //Output: Subtraction resolved! }) .catch(error => { console.error(error); });
Run node script.js
code to see the output.
Although Promise
is a great tool for handling asynchronous operations in JavaScript, there are certain limitations or cases where alternative approaches like async/await
may be more appealing.
Usually, the catch()
method is used to handle asynchronous errors in promises. However, tracking these errors across multiple chains can become difficult and complicated, as each promise has its own catch()
method. This leads to duplicated error-handling logic and makes it harder to manage errors consistently across the entire chain. async/await
provides a straightforward and centralized way of handling errors in asynchronous code by using the try/catch
blocks.
Dealing with a lengthy chain of asynchronous operations in promises can be difficult, especially when dynamic execution of subsequent promises is needed. This can create a code structure issue known as “promise hell,” which is comparable to callback hell. With async/await
, we can write asynchronous code that behaves like synchronous code. This approach makes code cleaner and easier to understand.
Long-chained promises can be difficult to follow or understand, particularly for developers who are new to the promise syntax. It can obscure the code’s logic and make debugging hard. With async/await, developers can write cleaner and more concise code, thus, making the codebase less difficult to understand.
promisfy()
methodPromisification refers to a transformation. It is the conversion of a callback-accepting function into a promise-returning function. Promisification aids in dealing with callback-based APIs while maintaining code consistency.
Node.js has an inbuilt utility module, util.promisify()
, that enables the creation of flexible promisification functions in JavaScript. util.promisify()
takes a single function parameter, which contains the callback-based function.
Let’s look at an example to better understand how to create a promisification function in Node.js.
First, we create two files, promisify.js
and promise.txt
.
In the promise.txt
file, we add the following text:
Promisification refers to a transformation. It is the conversion of a callback-accepting function into a promise-returning function. Promisification aids in dealing with callback-based APIs while maintaining code consistency.
Next, we add the following code to the promisify.js
file:
// Importing the fs module const fs = require('fs'); // Importing util module const util = require('util'); // Use promisify to fs.readFile to promise based method const readFile = util.promisify(fs.readFile); readFile('./promise.txt', 'utf8') // Reading the .txt file .then((text) => { console.log(text); }) // Log error if any .catch((err) => { console.log('Error', err); });
To read the files in the above example, we utilize the fs
module. Then, we use the util.promisify()
technique to transform the fs.readFile
into a promise-based function. Instead of a callback, the above method now returns a promise.
Now, run the following command: node promisify.js
. We see that the text from the promise.txt
file is logged to the console:
A client-side request can be transmitted to the server side via an Ajax request. Ajax is a particular type of asynchronous function used to build dynamic websites.
When a client requests data from the server, the server responds with the requested data, while the client continues to perform its current or subsequent action. As a result, the website runs without interruption. It’s also possible to use Ajax with a conventional callback interface:
const XMLHttpRequest = require('xhr2') let url = "https://api.github.com/users/kodecheff"; function makeAJAXCall(methodType, url, callback){ const xhr = new XMLHttpRequest() xhr.open(methodType, url, true); xhr.onreadystatechange = function(){ if (xhr.readyState === 4 && xhr.status === 200){ callback(xhr.response); } } xhr.send(); console.log("request sent to the server"); } // callback function function logUser(data){ console.log(data) } makeAJAXCall("GET", url, logUser); })
In contrast, promises are a technique used to control asynchronous activities. These asynchronous operations can be coordinated with other codes or asynchronous functions by keeping track of their states (pending, resolved, rejected) and the results (including any errors).
Promise and Ajax requests can both be wrapped inside of each other. Let’s say we want to begin an asynchronous operation, and once it is complete, begin another asynchronous operation (perhaps using the results of the first one).
In this type of situation, we would normally nest one inside the other. But unlike Ajax, a series of asynchronous tasks can be performed from a single promise outcome, eliminating callback hell or nested functions.
Another distinction between promises and Ajax requests is that Ajax can be utilized with different technologies, whereas promises require JavaScript to function.
In a Node.js-based environment, it’s preferable to use promises to callbacks. Unfortunately, callbacks are used in the majority of Node APIs. Node.js API callbacks are used as the last argument when calling functions, as shown in the examples below.
function perfectSquare (number, callback) { const bool = Number.isInteger(Math.sqrt(number)) if (!bool) { return callback(`Number ${number} is NOT a perfect square` ) } callback(`Number ${number} is a perfect square`) } // callback function function callback(data){ console.log(data) } perfectSquare(25, callback)
Here, the perfectSquare()
method checks if a number is a perfect square. Math.isInteger()
then determines if the value is an integer or a float number after passing the result of the square root check performed by Math.sqrt()
.
The bool
variable is used to store the Boolean (true or false) result of the Math.isInteger()
function. The callback
function is called to parse the intended data or result.
function perfectSquare (number) { return new Promise(function (fulfilled, rejected) { const bool = Number.isInteger(Math.sqrt(number)) if (!bool) { return rejected( new Error(`Number ${number} is NOT a perfect square`) ) } fulfilled( `Number ${number} is a perfect square` ) }) } perfectSquare(25).then(res => { console.log(res) }) })
The above code returns a Promise
instead of a callback. The rejected()
and fulfilled()
functions return a failed and successful operation, respectively. The .then()
method is used to utilize the result.
util.promisfy
methodBelow is the same example, but using the util.promisfy
method instead of an API callback or the promise API:
const util = require("util") let promiseCall = util.promisify(perfectSquare) promiseCall(5).then(res => { console.log(res) }) .catch(err => { console.log(err) })
When developing Node.js applications, it’s important to understand how to make optimal use of promises. Compared to the usual callback function, promises provide a clearer, more flexible, and better-organized manner of managing asynchronous operations.
In Node.js, we can use the util.promisify()
utility module to easily transform a standard function that receives a callback into a function that returns a promise.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.