Promises in JavaScript
Older Article
This article was published 9 years ago. Some information may be outdated or no longer applicable.
Promises let you write asynchronous code in a synchronous fashion. They also kill the callback hell (aka the pyramid of doom).
Callback hell
Callback hell is a common programming problem, especially in JavaScript, where asynchronous functions call callbacks that return values at some point in time. In Node.js this code is 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 uses error-first callbacks: the first parameter to the callback is an error, the second is the result upon success. The code above lists all files in /tmp, checks each one’s extension, and removes anything with a .tmp extension. Notice how the structure looks like a pyramid, hence “pyramid of doom.”
Flattening the callback pyramid
Promises flatten that pyramid and give us better error handling.
The key thing to remember: a promise is an object that could produce a single value at some point in the future. (To the best of my knowledge, Promises were first called Futures for this exact reason.)
Promises have three possible states: fulfilled, rejected, or pending. They produce either a resolved value or a reason for rejection.
As of ES2015, Promises are part of core JavaScript. We can construct them natively using resolve() and reject(). One fires when a promise is fulfilled, the other when it’s rejected.
Promise chaining
Promises use .then() methods that expect a callback to handle the resolved value. When a Promise object returns another Promise object, you can call .then() on it again. That lets you chain .then() calls.
Here’s a promise in action, making a call to a web service using the Fetch API. The Fetch API first returns a resource. We format that to JSON, and because response.json() also returns a Promise, we can chain another .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, with the fat arrow syntax the return keyword is implicit if the function body is a single line.
Resolving multiple promises
Sometimes you want to resolve multiple promises at once. Going back to the previous example, what if you want multiple characters from the Star Wars API? That’s where Promise.all() comes in. It returns a single promise when all the promises you’ve passed in have resolved. Those promises must be an iterable (like 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));
})
);
Native promises
In the previous example we used the Fetch API, which works with promises. But you can create your own promise function too:
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.
That code shows the core principle. As discussed earlier, a promise can be resolved, rejected, or pending (pending being the state it sits in before switching to one of the others). When we run this code and hit the resolve path, it yields the value of 1.
Catching errors
Promises help with error handling through the .catch() method. The brilliant thing about .catch(): if you’ve got multiple .then() statements chained together, a single .catch() at the end can capture errors from any of them.
Let’s rework the previous example to throw an actual error and see how .catch() handles it:
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 this displays the custom error.