Due to the many indentations needed, the nesting of callbacks in JavaScript leads to the pyramid of doom.
The promises introduced with ES2015 prevent such code structures but still allow asynchronous programming or asynchronous programs to be formulated so that they look like synchronous programs. So with promises, it’s generally easier to control an asynchronous program flow.
Promises themselves are nothing more than objects (of the Promise type) that serve as placeholders in JavaScript, so to speak, for the result of an asynchronous function (see code below). Instead of the asynchronous function itself being passed the callback handlers, it returns a Promise object that has access to two encapsulated callback functions: one to inform about the result value and one to inform about errors (usually the names of these callback functions are resolve() and reject() to clarify their intent). This simple but effective trick allows the synchronous code to continue working with the Promise object.
function asyncFunction() {
const promise = new promise(
function(resolve, reject) {
setTimeout(
() => {
const result = Math.floor(Math.random() * 100) + 1; // Random number
if(result >= 50) {
resolve(result); // Result
} else {
reject(`random number ${result} less than 50.`); // Error
}
}, 2000);
}
);
return promise;
}
Processing a Promise
So from the point of view of the calling code, you no longer pass a callback parameter to the asynchronous function. Instead, you now pass this parameter to the Promise object that you get from the asynchronous function as a return value. The Promise object provides the then() method for this purpose. It can be passed a callback handler for handling the result and a callback handler for handling errors, as shown here.
asyncFunction().then(
(result) => {
console.log(result);
},
(error) => {
console.error(error);
},
);
The relationship between the asynchronous function and the calling code is illustrated here.
As an alternative to specifying a callback function as the second parameter of then(), you can also pass it to the catch() method as in the listing below. This variant is now also used more often because catch() makes it clear at first glance where the error handling happens.
asyncFunction()
.then(
(result) => {
console.log(result);
})
.catch(
(error) => {
console.error(error);
}
)
);
Since ES2018, the finally() method is also available, which can be used to specify callback functions that are always executed, regardless of whether an error occurs or not.
asyncFunction()
.then(
(result) => {
console.log(result);
})
.catch(
(error) => {
console.error(error);
}
)
.finally(() => {
console.log('Always executed.');
});
);
Concatenating Promise Calls
Thanks to the Promise API’s Fluent API, calls to then() can be lined up relatively conveniently one after the other because then() always returns a Promise object itself as well. In the example below, the first callback handler returns the result of asyncFunction() multiplied by two, implicitly creating a Promise object. The return value of the callback handler is then used as input for the second callback handler.
asyncFunction()
.then((result) => {
return result * 2;
})
.then((result) => {
// Here result contains the result from above multiplied by two.
console.log(result);
});
Note: The then() method always implicitly returns a Promise object.
Thanks to this ability to concatenate promises, the pyramid of doom can also be avoided, as the next listing shows. By concatenating the then() method and passing appropriate callback functions, the code remains on one level.
asyncFunction()
.then((result) => {
// Contents of asyncFunction2
})
.then((result) => {
// Contents of asyncFunction3
})
.then((result) => {
// Contents of asyncFunction4
})
.then((result) => {
// Contents of asyncFunction5
});
The States of Promises
Internally, a Promise object assumes one of three states (see also figure below):
- Pending means that the asynchronous function is not yet completed.
- Fulfilled means that the asynchronous function completed successfully (and the Promise object contains a result value). This state is reached when the callback function resolve() has been called.
- Rejected means that the asynchronous function did not complete successfully or resulted in an error (and that the Promise object contains the reason for the error). This state is reached when the reject() callback function has been called.
In connection with states of promises, the term settled is also relevant: if a promise is settled, it means that it has either been fulfilled or rejected. In other words, it is no longer in the pending state.
Using Promises Helper Methods
In addition to the methods discussed, the Promise class also provides various static helper methods that are useful when working with promises, especially when calling various asynchronous functions in parallel that work with promises. For example, the Promise.all() method expects an array of promises (more precisely, an iterable of promises) and itself also returns a promise that is fulfilled if all submitted promises have been fulfilled and is rejected if any of the submitted promises have been rejected. If all promises have been fulfilled, the promise returned by Promise.all() returns the respective result values as an array. If one of the promises results in an error (i.e., it is rejected), this error is passed to Promise.all().
The next listing shows how to use the method: First, three promises are created here, with the first and third promises being fulfilled and the second promise being rejected. Therefore, the first call of the Promise.all() method (where promise number 2 is not passed) returns the result values of promise 1 and promise 3. The second call, however (promise number 2 is now also passed), results in an error.
// Promise 1 is "resolved"
const promise1 = new Promise((resolve, reject) => resolve('1'));
// Promise 2 is "rejected"
const promise2 = new Promise((resolve, reject) => reject('2'));
// Promise 3 is "resolved"
const promise3 = new Promise((resolve, reject) => resolve('3'));
Promise
.all([promise1, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: [ '1', '3' ]
Promise
.all([promise1, promise2, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: "Error: 2"
The Promise.race() method works similarly to Promise.all(), but it ends directly at the first promise that is fulfilled or rejected. Figuratively speaking, the individual promises passed thus run a race (hence the name of the method): the asynchronous function that is settled first—that is, either resolved or rejected—wins the race.
The next listing shows a simple example. The three promises defined at the beginning of the listing have been slightly modified compared to the previous listing to better illustrate the use of Promise.race(): promise 1 and promise 3 use the setTimeout() method to simulate two asynchronous functions that resolve after five seconds, while promise 2 rejects after two seconds. In other words, promise 2 wins the race, and Promise.race() returns the error thrown by promise 2.
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('1'), 5000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject('2'), 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve('1'), 5000);
});
Promise
.race([promise1, promise2, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: 1
The Promise.allSettled() method works slightly different from Promise.all(): it also expects an array or iterable of promises and also returns a promise, but unlike Promise. all(), the returned promise is not rejected once one of the given promises has been rejected. Instead, the promise is fulfilled when all passed promises have completed (i.e., when they settle), regardless of whether they have been fulfilled or rejected. As a result, the promise returns an array of objects, each of which contains the result of each promise. Here is an example.
const promise1 = new Promise((resolve, reject) => resolve('1'));
const promise2 = new Promise((resolve, reject) => reject('2'));
const promise3 = new Promise((resolve, reject) => resolve('3'));
Promise
.allSettled([promise1, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output:
// [
// { status: 'fulfilled', value: '1' },
// { status: 'fulfilled', value: '3' }
// ]
Promise
.allSettled([promise1, promise2, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output:
// [
// { status: 'fulfilled', value: '1' },
// { status: 'rejected', reason: '2' },
// { status: 'fulfilled', value: '3' }
// ]
In addition to the three methods presented thus far, ES2021 introduced the Promise. any() method. As the name of the method suggests, it returns a promise that is fulfilled as soon as one of the passed promises has been fulfilled and rejected only if all given promises have been rejected.
const promise1 = new Promise((resolve, reject) => resolve('1'));
const promise2 = new Promise((resolve, reject) => reject('2'));
const promise3 = new Promise((resolve, reject) => resolve('3'));
Promise
.any([promise1, promise2, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: 1
Promise
.any([promise2])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: "Error: AggregateError: All promises were rejected"
In addition to all these helper methods, Promise.resolve() and Promise.reject() provide two more methods to generate a promise that is fulfilled (Promise.resolve()) or rejected (Promise.reject()). For example, the previous code example can be rewritten using these two helper methods as shown here.
const promise1 = Promise.resolve('1');
const promise2 = Promise.reject('2');
const promise3 = Promise.resolve('3');
Promise
.any([promise1, promise2, promise3])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: 1
Promise
.any([promise2])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(`Error: ${error}`);
});
// Output: "Error: AggregateError: All promises were rejected"
Editor’s note: This post has been adapted from a section of the book JavaScript: The Comprehensive Guide by Philip Ackermann.
Comments