Promise is one of the best APIs for asynchronous operations in JavaScript. As a JavaScript developer, you need to be proficient in Promise. This article will summarise this knowledge, starting with a review of how JavaScript has handled asynchronous operations in the past? Then we’ll go into detail about the Promises object and its associated methods.

Let’s start with a look at JavaScript asynchronous concepts.

Asynchronous

What is asynchronous? A function is asynchronous if the expected result is not yet available to the caller when the function returns, but will be available in the future by some means (e.g. a callback function).

Asynchronous callbacks

Asynchronous callbacks, commonly known as callbacks, were a common way of handling asynchronous operations in JavaScript in the past, such as AJAX, where an HTTP request was initiated and the exact time when the server returned the response data depended on the client’s environment and there was a lot of uncertainty, when a callback function was used to trigger the callback function when the response data was returned.

This approach is common when there are multiple requests and subsequent requests need to wait for the response data of the previous request when the callback hell occurs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const asyncMessage = function (message, callback) {
    setTimeout(function () {
        console.log(message);
        callback();
    }, 1000);
};

asyncMessage("title", function () {
    asyncMessage("cate", function () {
        asyncMessage("content", function () {
            console.log("detail");
        });
    });
});

The presence of callback hell will cause many problems for project development.

  • It will lead to confusing logic, high coupling, one change will lead to all changes, and when nested, bugs are difficult to find.
  • You can’t use try.... .catch to catch exceptions.
  • Can’t use return to return real data

To avoid callback hell, the Promise object was created.

How Promise works

A promise object is an object that can be returned synchronously from an asynchronous function and will be in one of 3 possible states.

  • fulfilled: onFulfilled() will be called, i.e. the operation is complete (e.g. resolve() is called)
  • rejected: onRejected() will be called, i.e. the operation has failed (e.g. reject() is called)
  • pending: initial state, neither honoured nor rejected

If a promise does not hang (it has been resolved or rejected), it will be resolved. Sometimes resolved and resolved are used to indicate the same thing: not pending.

Once a promise has been determined, it cannot be redetermined, and calling resolve() or reject() again will have no effect. A determined promise has immutability.

For monitoring the state of a promise you can use a promise chain, i.e. in the fulfilled honoured state you can use the then method to get the honoured result, and in the rejected rejected state you can use the catch method to get the reason for the rejection.

1
2
3
4
5
const myPromise = new Promise(myExecutorFunc)
    .then(handleFulfilledA)
    .then(handleFulfilledB)
    .then(handleFulfilledC)
    .catch(handleRejectedAny);

It looks a little more elegant than the callback approach, and for needs that require multiple HTTP requests to be initiated for complete rendering, the code looks like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const getPost = () => fetch("https://jsonplaceholder.typicode.com/posts/1");
const getAuthor = (id) =>
    fetch("https://jsonplaceholder.typicode.com/users/" + id);
const getComment = (id) =>
    fetch("https://jsonplaceholder.typicode.com/users/" + id);

getPost() // #1.fetch post
    .then((postResponse) => postResponse.json()) // #2. get & return post json
    .then((postResponse) =>
        getAuthor(postResponse.id) // #3. fetch author
            .then((authorResponse) =>
                authorResponse
                    .json() // #4 get & return author json
                    .then((authorResponse) =>
                        getComment(postResponse.id) // #5 fetch comment
                            .then((commentResponse) => commentResponse.json()) // #6 get & return comment json
                            .then((commentResponse) => {
                                // #7 time to combine all results
                                return {
                                    postResponse,
                                    authorResponse,
                                    commentResponse,
                                }; // #8 combine & return all reponses
                            })
                    )
            )
            .then((results) => {
                // #9 read all responses
                console.log(results.postResponse);
                console.log(results.authorResponse);
                console.log(results.commentResponse);
            })
    )
    .catch((error) => console.log(error)); // # 10 error handling

Does the above code feel like déjà vu, originally intended to solve callback hell, but there seems to be a gap between the ideal and the reality.

So ES2021 adds new features to the Promise object, including: Promise.any(), Promise.all(), Promise.allSettled(), and Promise.race().

Promise.any()

Promise.any(promises) runs promise in parallel and resolves to the value of the first successfully resolved promise in the list of promises. Note that the Promise.any() method is still experimental and is not fully supported by all browsers.

Here is a look at how Promise.any() works.

1. How it works

Promise.any() can be used to perform independent asynchronous operations in a parallel and competitive manner to get the value of any first completed promise.

The function accepts an promise array (usually an iterable object) as an argument, as follows.

1
const anyPromise = Promise.any(promises);

When the first promise in the input promises is executed, anyPromise is immediately resolved to the value of that promise.

image

The value of the first promise can be extracted using the then method.

1
2
3
anyPromise.then((firstValue) => {
    firstValue; // 第一个 promise 完成后返回的值
});

The async/await syntax can also be used.

1
2
const firstValue = await anyPromise;
console.log(firstValue); // 第一个 promise 完成后返回的值

The promise returned by romise.any() is executed together with any first promise that is executed. Even if some promises are rejected, these rejections will be ignored.

image

However, if all promises in the input array are rejected, or if the input array is empty, then Promise.any() will rejected contain the set of rejection error reasons for the execution of the input promises.

image

2. Usage Guidelines

Before we dive into Promise.any(), let’s define 2 simple functions.

The function resolveTimeout(value, delay) will return a promise that has resolved after delay time has elapsed.

1
2
3
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}

The function rejectTimeout(reason, delay) returns a promise that has a reject after the delay time has elapsed.

1
2
3
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}

Next, try Promise.any() using the 2 helper functions defined above.

2.1 Completing all promises

Try running the first parsed list as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];
const promise = Promise.any([
    resolveTimeout(fruits, 1000),
    resolveTimeout(vegetables, 2000),
]);

// 等待...
const list = async () => {
    const result = await promise;
    console.log(result);
};

// 1 秒之后
list(); // ['potatoes', 'tomatoes']

promise .any([...]) returns a promise that parses the array fruits in 1 second, since the promise that parses the fruits finishes first.

The second is a promise that resolves to the array vegetables in 2 seconds, and its value is ignored.

2.2 A promise is rejected

The first promise above is rejected with an exception, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const vegetables = ["oranges", "apples"];

const promise = Promise.any([
    rejectTimeout(new Error("fruits is empty"), 1000),
    resolveTimeout(vegetables, 2000),
]);

// 等待...
const list = async () => {
    const result = await promise;
    console.log(result);
};

// 2 秒之后
list(); // [ 'oranges', 'apples' ]

In the above code, the first promise is rejected after 1 second and it is easy to see from the result of the execution that Promise.any() skips the first promise that is rejected and waits for the second promise that finishes 2 seconds later.

2.3 All promises are rejected

Here’s what happens when all promises are rejected, with the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}
const promise = Promise.any([
    rejectTimeout(new Error("fruits is empty"), 1000),
    rejectTimeout(new Error("vegetables is empty"), 2000),
]);

// 等待...
const list = async () => {
    try {
        const result = await promise;
        console.log(result);
    } catch (aggregateError) {
        console.log(aggregateError);
        console.log(aggregateError.errors);
    }
};

list(); // [AggregateError: All promises were rejected]

Summary

Promise.any() can be used to execute independent asynchronous operations in parallel in a competitive manner to obtain the value of any first promise that completes successfully. If all input promises to Promise.any() have been rejected, then the promise returned by the helper function will also be rejected as an aggregate error, which contains the reason for the rejection of the input promise in a special property AggregateError: aggregateError.errors .

Promise.all()

method Promise.all(promises), can process multiple promises at once in parallel and return only one promise instance, the result of the resolve callback for all the promises entered is an array.

Here’s how Promise.all() works.

1. How it works

Promise.all() is a built-in helper function that takes a set of promises (or an iterable object) and returns a promise: Promise.all() is a built-in helper function that takes a set of promises (or an iterable object) and returns a promise.

1
const allPromise = Promise.all([promise1, promise2, ...]);

The value of the first promise can be extracted using the then method.

1
2
3
allPromise.then((values) => {
    values; // [valueOfPromise1, valueOfPromise2, ...]
});

The async/await syntax can also be used.

1
2
const values = await allPromise;
console.log(values); // [valueOfPromise1, valueOfPromise2, ...]

The way in which the promise returned by Promise.all() was parsed or rejected.

If allPromise are all successfully parsed, then allPromise will use as its result an array containing the values of each promise after it has been executed. The order of the promises in the array is important - the implemented values will be obtained in this order.

image

But if at least one promise is rejected, then allPromise is immediately rejected for the same reason (without waiting for the execution of other promises).

image

If all promises are rejected, wait for all promises to finish, but only return the reject reason for the first promise that was rejected.

image

2. Usage Guidelines

Before we dive into Promise.all(), let’s define 2 simple functions.

The function resolveTimeout(value, delay) will return a promise that has resolved after delay time has elapsed.

1
2
3
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}

The function rejectTimeout(reason, delay) returns a promise that has a reject after the delay time has elapsed.

1
2
3
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}

Next, try Promise.all() using the 2 helper functions defined above.

2.1 Completing all promises

An promise array allPromise is defined below, and all promises are capable of successful resolve values, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];

const allPromise = [
    resolveTimeout(fruits, 2000),
    resolveTimeout(vegetables, 1000),
];
const promise = Promise.all(allPromise);

// 等待... 2秒后
const list = async () => {
    try {
        const result = await promise;
        console.log(result);
    } catch (error) {
        console.log(error.errors);
    }
};

list(); // [ [ 'potatoes', 'tomatoes' ], [ 'oranges', 'apples' ] ]

The result of the above execution shows that the resolve array of promise returned by Promise.all() is composed in the order of allPromise before it is executed.

The order of the promise arrays directly affects the order of the result, regardless of the order in which the execution of promise completes.

2.2 A promise is rejected

The first promise of the array allPromise above is rejected with an exception, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const promise = Promise.all([
    rejectTimeout(new Error("fruits is empty"), 5000),
    resolveTimeout(vegetables, 1000),
]);

// 等待...
const list = async () => {
    try {
        const result = await promise;
        console.log(result);
    } catch (error) {
        console.log(error);
    }
};

list(); // Error: fruits is empty

However, after 5 seconds, the first promise is rejected due to an exception, causing allPromise to be rejected as well, and returning the same error message as the first promise: Error: fruits is empty, even though the value of the second promise after 1 second is completed, the value of the second promise is not accepted.

Next, all the promises in the array allPromise are thrown with an exception and rejected, and the order of rejected is adjusted by the timer as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const promise = Promise.all([
    rejectTimeout(new Error("fruits is empty"), 5000),
    rejectTimeout(new Error("vegetables is empty"), 1000),
]);

// 等待...
const list = async () => {
    try {
        const result = await promise;
        console.log(result);
    } catch (error) {
        console.log(error);
    }
};

After 5 seconds, the execution completes and the result is Error: vegetables is empty, it is easy to see that allPromise was rejected because the promise that was rejected first.

This behaviour of > Promise.all() is known as fast failure, and if at least one promise in the promise array is rejected, then the returned promise is also rejected. If all of the promise arrays are rejected, then the returned promise is rejected because of the one that was rejected first.

Summary

Promise.all() is the best way to perform asynchronous operations in parallel and get all the resolve values, ideal for situations where you need to get the results of the asynchronous operations simultaneously to perform the next operation.

Promise.allSettled()

method Promise.allSettled(promises), returns a promise after all the given promises have been fulfilled or rejected, with an array of objects, each representing the corresponding promise result.

Here’s how Promise.allSettled() works.

1. How it works

Promise.allSettled() can be used to execute independent asynchronous operations in parallel and collect the results of those asynchronous operations.

The function accepts an promise array (or usually an iterable one) as an argument, as follows.

1
const statusesPromise = Promise.allSettled(promises);

When all input promises have been fulfilled or rejected, statusesPromise is resolved to an array with its status.

  • { status: 'fulfilled', value:value } : if the corresponding promise has been fulfilled
  • { status: 'rejected', reason: reason } : if the corresponding promise was rejected

image

The state of all promises can be extracted using the then method.

1
2
3
statusesPromise.then((statuses) => {
    statuses; // [{ status: '...', value: '...' }, ...]
});

The async/await syntax can also be used.

1
2
3
const statuses = await statusesPromise;

statuses; // [{ status: '...', value: '...' }, ...]

The promise returned by Promise.allSettled() is always realized with a range of states, regardless of whether some (or all) of the input promises are rejected.

The big difference between Promise.allSettled() and Promise.all(): Promise.allSettled() will never be rejected.

2. Usage Guidelines

Before we dive into the use of Promise.allSettled(), let’s define 2 simple functions.

1
2
3
4
5
6
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}

Next, try Promise.allSettled() using the 2 helper functions defined above.

2.1 Completing all promises

An promise array statusesPromise is defined below, and all promises are capable of successful resolve values, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];

const statusesPromise = Promise.allSettled([
    resolveTimeout(fruits, 2000),
    resolveTimeout(vegetables, 1000),
]);

// 等待 2 秒 ...
const list = async () => {
    try {
        const statuses = await statusesPromise;
        console.log(statuses);
    } catch (error) {
        console.log(error);
    }
};

list(); // [{ status: 'fulfilled', value: [ 'potatoes', 'tomatoes' ] },{ status: 'fulfilled', value: [ 'oranges', 'apples' ] }]

From the results of the above execution Promise.allSettled() returns an array of resolve states of a promise in the order in which the statusesPromise were composed before it was executed.

2.2 A promise is rejected

The first promise above is rejected with an exception, with the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const fruits = ["potatoes", "tomatoes"];

const statusesPromise = Promise.allSettled([
    resolveTimeout(fruits, 2000),
    rejectTimeout(new Error("Vegetables is empty"), 1000),
]);

// 等待 2 秒 ...
const list = async () => {
    try {
        const statuses = await statusesPromise;
        console.log(statuses);
    } catch (error) {
        console.log(error);
    }
};

list(); // // [{ status: 'fulfilled', value: ['potatoes', 'tomatoes'] },{ status: 'rejected', reason: Error('Vegetables is empty') }]

Even if the second promise in the input array is rejected, statusesPromise can still successfully parse the state array.

2.3 All promises are rejected

All of the above promises with exceptions are rejected, with the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const statusesPromise = Promise.allSettled([
    rejectTimeout(new Error("Fruits is empty"), 2000),
    rejectTimeout(new Error("Vegetables is empty"), 1000),
]);

// 等待 2 秒 ...
const list = async () => {
    try {
        const statuses = await statusesPromise;
        console.log(statuses);
    } catch (error) {
        console.log(error);
    }
};

list(); // // [{ status: 'rejected', reason: Error('Fruits is empty') },{ status: 'rejected', reason: Error('Vegetables is empty') }]

Summary

Promise.allSettled() is a good choice when parallel and independent asynchronous operations need to be performed and all results collected, even if some asynchronous operations may fail.

Promise.race()

method Promise.race(promises), as the name implies, means race, Promise.race([p1, p2, p3] fetches the result of whichever of the promise arrays completes faster, regardless of whether the result itself is a successful fulfillment or a failed rejection state, and outputs only the fastest promise.

Here’s how Promise.race() works.

1. How it works

Promise.race() returns a promise that will be fulfilled or rejected once one of the promises in the iterator has been fulfilled or rejected.

The function accepts an promise array (or usually an iterable) as an argument, as follows.

1
const racePromise = Promise.race(promises);

When one of all input promises is quickly fulfilled or rejected, racePromise resolves the fast-completing promise result (fulfilled or rejected) as follows.

image

image

The result of racePromise can be extracted using the then method.

1
2
3
racePromise.then((fastValue) => {
    fastValue // fast completion promise
});

The async/await syntax can also be used.

1
2
3
const fastPromise = await racePromise;

fastPromise; // fast-completing promise

Promise.race() Returns a promise with the same information as the first promise to complete.

Difference between Promise.race() and Promise.any(): Promise.race() looks for the first fulfilled or rejected promise in the list of promises; Promise.any() looks for the first fulfilled promise from the list of promises.

2. Usage Guidelines

Before we dive into the use of ``Promise.race()`, let’s define 2 simple functions as well.

1
2
3
4
5
6
function resolveTimeout(value, delay) {
    return new Promise((resolve) => setTimeout(() => resolve(value), delay));
}
function rejectTimeout(reason, delay) {
    return new Promise((r, reject) => setTimeout(() => reject(reason), delay));
}

Next, try Promise.race() using the 2 helper functions defined above.

2.1 Completing all promises

An promise array racePromise is defined below, and all promises are capable of successful resolve values as follows

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const fruits = ["potatoes", "tomatoes"];
const vegetables = ["oranges", "apples"];

const racePromise = Promise.race([
    resolveTimeout(fruits, 5000),
    resolveTimeout(getables, 1000),
]);

// wait 1 second ...
const list = async () => {
    try {
        const fastPromise = await racePromise;
        console.log(fastPromise);
    } catch (error) {
        console.log(error);
    }
};

list(); // [ 'oranges', 'apples' ]

From the above execution Promise.race() returns the resolve result of the fastest performing promise.

2.2 A promise is rejected

The first promise above is rejected with an exception, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const fruits = ["potatoes", "tomatoes"];

const racePromise = Promise.race([
    resolveTimeout(fruits, 2000),
    rejectTimeout(new Error("Vegetables is empty"), 1000),
]);

// 等待 1 秒 ...
const list = async () => {
    try {
        const fastPromise = await racePromise;
        console.log(fastPromise);
    } catch (error) {
        console.log(error);
    }
};

list(); // Error: Vegetables is empty

From the above result, the first promise to complete is rejected, so the promise returned by fastPromise is also rejected.

The following extends the promise time for rejected to 5 seconds, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const fruits = ["potatoes", "tomatoes"];

const racePromise = Promise.race([
    resolveTimeout(fruits, 2000),
    rejectTimeout(new Error("Vegetables is empty"), 5000),
]);

// 等待 2 秒 ...
const list = async () => {
    try {
        const fastPromise = await racePromise;
        console.log(fastPromise);
    } catch (error) {
        console.log(error);
    }
};

list(); // [ 'potatoes', 'tomatoes' ]

As you can see from the above results, the fastest completed promise fulfilled resolve, so the promise returned by fastPromise also fulfilled resolve.

2.3 All promises are rejected

All the promises above are rejected with an exception, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const racePromise = Promise.race([
    rejectTimeout(new Error("Fruits is empty"), 2000),
    rejectTimeout(new Error("Vegetables is empty"), 1000),
]);

// 等待 1 秒 ...
const list = async () => {
    try {
        const fastPromise = await racePromise;
        console.log(fastPromise);
    } catch (error) {
        console.log(error);
    }
};

list(); // Error: Vegetables is empty

From the results, although both promises were rejected, the promise returned by fastPromise was the fastest to be rejected.

3. Usage scenarios

3.1. Performance testing

In projects with asynchronous operations, you can use Promises to test the performance of network or database requests by using Promise.race() to test the responsiveness of the two different methods.

3.2 Optimal choices

For example fetching the same type of data with multiple requesting servers, sending requests to multiple servers at the same time and rendering their data as soon as one of them completes its work, to achieve the effect of choosing the best route. This is done by using Promise.race() to execute promise at the same time and finish as soon as the first one succeeds.

Summary

Promise.race() executes a callback function for the first resolved and rejected promise, while Promise.any() executes a callback function for the first fulfilled promise and rejects a special property AggregateError if the unfulfilled promise is rejected.