Learn how to create and work with Promises in Node.js
Posted
Updated
Europe’s developer-focused job platform
Let companies apply to you
Developer-focused, salary and tech stack upfront.
Just one profile, no job applications!
A promise is a placeholder for a value that will be available in the future, so the result of an asynchronous task can be handled once it has finished.
Promises make writing asynchronous code easier and are an improvement to the callback pattern (please google for callback hell
).
Since ES6 promises are a standard part of Javascript and with async/await
(ES8) they are used in async functions .
This article is based on Node v16.14.0.
💰 The Pragmatic Programmer: journey to mastery. 💰 One of the best books in software development, sold over 200,000 times.
To understand Promises it is important to understand the difference between synchronous and asynchronous code first.
Synchronous code executes in the sequence it is written, code statements wait until the ones before them have finished. Hence, synchronous code is considered blocking in Node.js. Blocking could be in some rare cases considered useful, like reading important configuration on start-up before anything else runs, but the application is unresponsive until this synchronous task is finished. Therefore, not applicable on long-running tasks, like making an HTTP call.
Asynchronous code works by starting a task, and letting it complete in the background while other code is still able to execute. When the async code has completed, the handler function (callback) is immediately executed with the result from the async code. Hence, asynchronous code is non-blocking, because it does not prevent the rest of your code from executing, while the asynchronous task is running in the background. With async code, we don't know when or if the task will complete successfully. The callback of the async code will be called as soon as the result is available, or when an error has occurred.
Once you an async process has started, like an HTTP request, filesystem access, or something similar, you are given something that will notify the caller when that process has completed. A Promise is that "something". A promise is a placeholder for a value that will be available in the future.
Promises allow to handle the results of asynchronous code, like callbacks. Unlike callbacks, the async code with promises is easier to read, maintain, and reason about. Consider these examples, five consecutive API calls with error handling.
Promises
fetch('url')
.then(() => fetch('url'))
.then(() => fetch('url'))
.then(() => fetch('url'))
.then(() => fetch('url'))
.then(() => console.log('all done'))
.catch(err => console.log(err));
Callbacks
fetchCallback('url', err => {
if (err) return console.log(err);
fetchCallback('url', err => {
if (err) return console.log(err);
fetchCallback('url', err => {
if (err) return console.log(err);
fetchCallback('url', err => {
if (err) return console.log(err);
console.log('all done');
});
});
});
});
As you can see, the code is more legible with Promises.
We can interact with the result of the Promise by chaining together handlers, that will either wait for the Promise to be fulfilled with a value, or rejected with the first error thrown.
fetch('url')
.then(response => console.log(response.status))
.catch(error => console.log(error));
In the above code example fetch
returns a Promise, and the Promise API allows us to chain the then
and catch
handlers.
Your Promise chain should include a catch handler to deal with any Promises that are rejected in the chain.
To handle errors with catch
is best practice.
In future versions of Node.js, unhandled Promise rejections will crash your application with a fatal exception.
A Promise is in one of these three states:
A new Promise can be created by initializing one with the Promise
constructor:
const myPromise = new Promise((resolve, reject) => {
// do something asynchronous
});
The Promise constructor takes two functions as arguments, resolve and reject.
We can do the asynchronous task, and then call either resolve (with the result if successful) or reject(with the error).
The constructor returns a Promise object, which can then can be chained with then
and catch
methods.
Let's have a look at some example:
const fs = require('fs');
const myPromise = new Promise((resolve, reject) => {
fs.readFile('example.json', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
myPromise
.then(data => console.log(data))
.catch(err => console.log(err));
In the code example above, we wrapped fs.readFile
in a Promise
.
If reading the file encountered an error, we pass it to reject, otherwise we pass the data obtained from the file to resolve.
Calling resolve
passes the data to our .then
handler, and reject
passes the error to the .catch
handler.
Combining multiple Promises is one of the big advantages of Promises over using callbacks. It is difficult to orchestrate multiple callbacks together, whereas with Promises it is much more readable, and error handling is standardized between the different Promises.
Let's have a look at an example for fetching the json placeholder API to get some todos.
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log(err));
In the example above we fetch some JSON data via an HTTP request. The fetch
function returns a promise, which will either resolve or reject.
The attached then
handles the response by fetch, when it resolves. The response body has a json
method for parsing the response from JSON to an object.
The json
method returns a promise of its own, which handle by attaching another then
handler, and in case of error we attach a catch
handler and log the error.
then
and catch
methods to a Promise in order to execute code when the state changes.Thanks for reading and if you have any questions, use the comment function or send me a message @mariokandut.
If you want to know more about Node, have a look at these Node Tutorials.
Never miss an article.