Promises in JavaScript are a powerful concept that allow us to essentially write asynchronous code in a synchronous fashion and therefore provide us with additional benefits such as the elimination of the callback hell (aka pyramid of doom).
Callback hell or the pyramid of doom is a common programming problem especially when working with JavaScript whereby asynchronous functions call callbacks – callbacks will then return a value at some point in time. In Node.js this code would be a typical example:
const fs = require('fs');
const path = require('path');
fs.readdir('/tmp', (error, files) => {
if (error) {
console.error(`Error reading folder ${error}`);
} else {
files.forEach((filename) => {
if (path.extname(filename) === '.tmp') {
fs.unlink(filename, (error) => {
if (error) {
console.error(`Error deleting file ${error}`);
} else {
console.info(`Successfully deleted ${filename}`);
}
});
}
});
}
});
Node.js utilises error first callbacks, which means that the first parameter to the callback function is going to be an error while the second one will be the actual result of the operation upon success. In the code above we are listing all the files in the /tmp
folder and for each file we are checking the extension and if the file has the .tmp
extension we are removing it. Notice how the whole code structure looks like a pyramid as well henceforth the alternative name 'pyramid of doom'.
Promises help us flatten out this pyramid and also gives us better error handling capabilities.
The most important thing to remember is that a promise is basically an object that could produce us a single value at some point in time in the future. (To the best of my knowledge Promises were also first called Futures for this exact reason).
Promises have three possible states: fulfilled
, rejected
or pending
and it can produce us a resolved value, or a reason for rejection.
As of ES2015 Promises form part of the core JavaScript language and we can finally construct promises natively by making use of the resolve()
and reject()
methods – one gets called when a promise is fulfilled and the other is going to be called when the promise is rejected.
Promises are also using .then()
methods which expect a callback to handle the resolved value. One key feature of promises is that when a Promise object returns another Promise object we can call .then()
method on it again, effectively this allows us to chain .then()
calls.
Let's take a look at a promise in action by making a call to a web service and using the Fetch API. The Fetch API first returns a resource, we then format that to JSON and by calling response.json()
we also return a Promise object, which will again allow us to call .then():
Please note that the Fetch API is available in the browser only as it's a Web API.
fetch('https://swapi.co/api/people/1')
.then((response) => response.json())
.then((response) => console.log(response));
Remember, by using the fat arrow syntax the return keyword is implicit if we specify the function body in one line.
There are scenarios when we'd like to resolve multiple promises – going back to the previous example what if we'd like to get multiple characters returned from the Star Wars API? In this case we can utilise Promise.all()
, which returns a single promise when all promises have been resolved. Please note that the promises that we pass in to Promise.all()
must be an iterable (for example an array):
const urls = ['https://swapi.co/api/people/1', 'https://swapi.co/api/people/4'];
Promise.all(
urls.map((url) => {
return fetch(url)
.then((response) => response.json())
.then((response) => console.log(response.name))
.catch((error) => console.error(error));
})
);
In the previous example, we have been using the Fetch API which works by using promises. We can however create our own promise function as well:
function myPromise() {
return new Promise((resolve, reject) => {
const error = false; // only to mimic an error
if (!error) {
resolve(1);
} else {
reject(new Error(error));
}
});
}
myPromise().then((response) => console.log(response));
Please note that in the code sample above we are mimicking an error by assigning a string to a variable with a name of error.
The above code demonstrates the core principle of promises. As we discussed earlier, a promise can either be resolved or rejected (or be pending, essentially that's the state the promise uses to switch to any of the other states). When we run the code above and arrive to part where we are resolving the promise, it'll yield us the value of 1.
Promises help us with error handling as well by introducing the .catch()
method. The wonderful thing about .catch()
is that if we are having multiple .then()
statements in our promise – i.e. we are using a chain of promises – no matter where the error happens, one single .catch()
statement is capable of capturing the error.
Let's rework the previous example and throw an actual error and let's see how we can apply .catch()
to catch the error:
function myPromise() {
return new Promise((resolve, reject) => {
const error = true; // only to mimic an error
if (!error) {
resolve(1);
} else {
reject(new Error('Custom error occurred'));
}
});
}
myPromise()
.then((response) => console.log(response))
.catch((error) => console.error(error));
Running the above code will display the custom error.