Piotr Gajek
written byPiotr Gajek
posted on February 13, 2024
Technical Architect and Full-stack Salesforce Developer. He started his adventure with Salesforce in 2017. Clean code lover and thoughtful solutions enthusiast.

Advanced Promises in LWC

Advanced Promises LWC

Introduction

Before we start, you have to understand what promises are, the difference between then/catch and async/await, and the best practices when working with promises. These two articles can help you out:

  1. Promises in LWC
  2. Then vs Async Await in LWC

Promise.all

The Promise.all() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the input's promises are fulfilled (including when an empty iterable is passed), with an array of the fulfillment values. It rejects when any of the input's promises is rejected, with this first rejection reason. ~ [2] MDN Web Docs

Example

const promiseA = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseA works!'
}), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseB works!'
}), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseC works!'
}), 250));

// Promise.all() [...] takes an iterable of promises as input
// it can be an array of promises [promiseA, promiseB, promiseC];
// and returns a single Promise
// single promise can be resolved with then or async/await

// then
Promise.all([promiseA, promiseB, promiseC])
    .then(results => {
        results.forEach(result => console.log(result));
    })
    .catch(error => {
         console.log(`Error: ${error}`);
    });

// or async/await
try {
    const results = await Promise.all([promiseA, promiseB, promiseC]);
    results.forEach(result => console.log(result));
} catch (error) {
    console.log(`Error: ${error}`);
}
OUTPUT

{message: 'promiseA works!'}
{message: 'promiseB works!'}
{message: 'promiseC works!'}

The results variable is an array ([]) that contains the results of each promise. The order of passed promises indicates the order of results.

promiseA => results[0] => {message: 'promiseA works!'}
promiseB => results[1] => {message: 'promiseB works!'}
promiseC => results[2] => {message: 'promiseC works!'}

What will happen if any of the promises fail?

const promiseA = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseA works!'
}), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseB works!'
}), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseC something happens!'
}), 250));

Promise.all([promiseA, promiseB, promiseC])
    .then(results => {
        results.forEach(result => console.log(result));
    })
    .catch(error => {
         console.log(`Error: ${JSON.stringify(error)}`);
    });
OUTPUT

Error: {"message": "promiseC something happen!"}

Promise.all() rejects when ANY of the passed promises rejects, with the first rejection reason.

Promise.all - when to use?

It can be useful for aggregating the results of multiple promises. It is typically used when there are multiple related asynchronous tasks that the overall code relies on to work successfully — all of which we want to fulfill before the code execution continues. ~ [2] MDN Web Docs

Use Promise.all() when you need to get the results of many asynchronous methods at once, and all Promises should be fulfilled successfully (are dependent).

Below you can find an example, where we need to wait until all page settings are loaded at once.

import { LightningElement } from "lwc";

import getUserPermissions from "@salesforce/apex/MyController.getUserPermissions";
import getUserData from "@salesforce/apex/MyController.getUserData";
import getPageSettings from "@salesforce/apex/MyController.getPageSettings";

export default class MyComponent extends LightningElement {
    isLoading = true;

    userPermissions = [];
    userData = {};
    pageSettings = {}

    connectedCallback() {
        Promise.all([
            getUserPermissions,
            getUserData,
            getPageSettings
        ]).then(results => {
            this.userPermissions = results[0];
            this.userData = results[1];
            this.pageSettings = results[2];
        }).catch(err => {
            console.error(error);
        }).finally(() => {
            this.isLoading = false;
        });
    }
}

Promise.all - best practices

❌❌❌

import { LightningElement } from "lwc";

import getUserPermissions from "@salesforce/apex/MyController.getUserPermissions";
import getUserData from "@salesforce/apex/MyController.getUserData";
import getPageSettings from "@salesforce/apex/MyController.getPageSettings";

export default class MyComponent extends LightningElement {
    isLoading = true;

    userPermissions = [];
    userData = {};
    pageSettings = {}

    connectedCallback() {
        Promise.all([
            getUserPermissions,
            getUserData,
            getPageSettings
        ]).then(results => {
            this.userPermissions = results[0];
            this.userData = results[1];
            this.pageSettings = results[2];
        }).catch(err => {
            console.error(error);
        }).finally(() => {
            this.isLoading = false;
        });
    }
}

A good practice is to use the Destructuring assignment.
It allows you to unpack values from an array and make the code easier to read instead of using array indexes [0], [1], etc.

✅✅✅

import { LightningElement } from "lwc";

import getUserPermissions from "@salesforce/apex/MyController.getUserPermissions";
import getUserData from "@salesforce/apex/MyController.getUserData";
import getPageSettings from "@salesforce/apex/MyController.getPageSettings";

export default class MyComponent extends LightningElement {
    isLoading = true;

    userPermissions = [];
    userData = {};
    pageSettings = {}

    connectedCallback() {
        Promise.all([
            getUserPermissions,
            getUserData,
            getPageSettings
        ]).then(([userPermissions, userData, pageSettings]) => {
            this.userPermissions = userPermissions;
            this.userData = userData;
            this.pageSettings = pageSettings;
        }).catch(err => {
            console.error(error);
        }).finally(() => {
            this.isLoading = false;
        });
    }
}

Promise.allSettled

The Promise.allSettled() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the input's promises settle (including when an empty iterable is passed), with an array of objects that describe the outcome of each promise. ~ [2] MDN Web Docs

const promiseA = new Promise((resolve, reject) => setTimeout(() => reject('promiseA something happened'), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve('promiseB works!'), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => reject('promiseC something happend'), 250));

// Promise.allSettled() [...] takes an iterable of promises as input
// it can be an array of promisses [promiseA, promiseB, promiseC];
// and returns a single Promise
// single promise can be resolved with then or async/await

// then
Promise.allSettled([promiseA, promiseB, promiseC])
    .then(results => {
        results.forEach(result => console.log(result));
    });

// or async/await
const results = await Promise.allSettled([promiseA, promiseB, promiseC]);
results.forEach(result => console.log(result));
OUTPUT

{status: 'rejected', reason: 'promiseA something happened'}
{status: 'fulfilled', value: 'promiseB works!'}
{status: 'rejected', reason: 'promiseC something happened'}

The results variable is an array ([]) that contains the results of each promise. The order of passed promises indicates the order of results.

promiseA => results[0] => {status: 'rejected', reason: 'promiseA something happened'}
promiseB => results[1] => {status: 'fulfilled', value: 'promiseB works!'}
promiseC => results[2] => {status: 'rejected', reason: 'promiseC something happened'}

Promise.allSettled() NEVER rejects; the fulfillment value is an array of objects, each describing the outcome of one promise (status, value/reason).

Promise.allSettled - when to use?

Promise.allSettled() is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully, or you'd always like to know the result of each promise. ~ [3] MDN Web Docs

Use Promise.allSettled() when you need to get results of many asynchronous methods at once, but it does not matter if all will be successful or not (promises are independent).

Promise.all vs Promise.allSettled

Promise.allSettled() is typically used when you have multiple asynchronous tasks that are not dependent on one another to complete successfully ~ [3] MDN Web Docs

Promise.all() may be more appropriate if the tasks are dependent on each other, or if you'd like to immediately reject upon any of them rejecting. ~ [3] MDN Web Docs

const promiseA = new Promise((resolve, reject) => setTimeout(() => resolve('promiseA works!'), 250));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve('promiseB works!'), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => reject('promiseC something happend'), 200));

Promise.all([promiseA, promiseB, promiseC])
    .then(results => {
        results.forEach(result => console.log(result));
    })
    .catch(error => {
         console.log(`Error: ${error}`);
    });

Promise.allSettled([promiseA, promiseB, promiseC])
    .then(results => {
        results.forEach(result => console.log(result));
    });
OUTPUT

// Promise.all
Error: promiseC something happened

// Promise.allSettled
{status: 'fulfilled', value: 'promiseA works!'}
{status: 'fulfilled', value: 'promiseB works!'}
{status: 'rejected', reason: 'promiseC something happened'}

Explanation

Promise.all() will be rejected if any of the passed promises are rejected, such as promiseC.

Error: promiseC something happened

Promise.allSettled will ALWAYS be fulfilled, and the catch block will NEVER be executed. Promise.allSettled gives you a result of each promise independently with status and reason/value.

{status: 'fulfilled', value: 'promiseA works!'}
{status: 'fulfilled', value: 'promiseB works!'}
{status: 'rejected', reason: 'promiseC something happened'}

Promise.any

The Promise.any() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when any of the input's promises are fulfilled, with this first fulfillment value. ~ [3] MDN Web Docs

Example

const promiseA = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseA something happened!'
}), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseB works!'
}), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseC works!'
}), 250));

// Promise.any() [...] takes an iterable of promises as input
// it can be an array of promisses [promiseA, promiseB, promiseC];
// and returns a single Promise
// single promise can be resolved with then or async/await

// then
Promise.any([promiseA, promiseB, promiseC])
    .then(result => {
        console.log(result);
    })
    .catch(error => {
         console.log(`Error: ${error}`);
    });

// or async/await
try {
    const result = await Promise.any([promiseA, promiseB, promiseC]);
    console.log(result);
} catch (error) {
    console.log(`Error: ${error}`);
}
OUTPUT

{message: 'promiseC works!'}
promiseA - 200 ms - rejected
promiseC - 250 ms - fulfilled <= first fulfillment value
promiseB - 300 ms - fulfilled

This returned promise fulfills when any of the input's promises fulfills, with this first fulfillment value.

In our example, the first fulfillment value comes from promiseC.

Important!
The promise returned by Promise.any() fulfills any first fulfilled promise. Even if some promises get rejected, these rejections are ignored [5].

What will happen if ALL of the promises fail?

It rejects when all of the input's promises are rejected (including when an empty iterable is passed), with an AggregateError containing an array of rejection reasons. ~ [2] MDN Web Docs

const promiseA = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseA something happens!'
}), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseB something happens!'
}), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseC something happens!'
}), 250));

Promise.any([promiseA, promiseB, promiseC])
    .then(result => {
        console.log(result);
    })
    .catch(error => {
         console.log(`Error: ${error}`);
    });
OUTPUT

Error: AggregateError: All promises were rejected

Promise.any() rejects when ALL of the passed promises are rejected.

Promise.any - when to use?

This method is useful for returning the first promise that fulfills. It short-circuits after a promise fulfills, so it does not wait for the other promises to complete once it finds one. ~ [3] MDN Web Docs

Assume that you have configuration stored in three different systems Salesforce, AWS, and Firebase. It's important to you to load the page as soon as possible, so you call all three services but need only a response from the fastest one.

import { LightningElement } from "lwc";

import getConfigFromSalesforce from "@salesforce/apex/MyController.getConfigFromSalesforce";
import getConfigFromAWS from "@salesforce/apex/MyController.getConfigFromAWS";
import getConfigFromFirebase from "@salesforce/apex/MyController.getConfigFromFirebase";

export default class MyComponent extends LightningElement {
    settings = {}

    connectedCallback() {
        Promise.any([
            getConfigFromSalesforce,
            getConfigFromAWS,
            getConfigFromFirebase
        ]).then(result => {
            this.settings = result;
        })
        .catch(err => {
            console.error(error);
        });
    }
}

Promise.race

The Promise.race() static method takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles.

Example

const promiseA = new Promise((resolve, reject) => setTimeout(() => reject({
    message: 'promiseA something happened!'
}), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseB works!'
}), 300));

const promiseC = new Promise((resolve, reject) => setTimeout(() => resolve({
    message: 'promiseC works!'
}), 250));

// Promise.race() [...] takes an iterable of promises as input
// it can be an array of promisses [promiseA, promiseB, promiseC];
// and returns a single Promise
// single promise can be resolved with then or async/await

// then
Promise.race([promiseA, promiseB, promiseC])
    .then(result => {
        console.log(result);
    })
    .catch(error => {
         console.log(`Error: ${JSON.stringify(error)}`);
    });

// or async/await
try {
    const result = await Promise.race([promiseA, promiseB, promiseC]);
    console.log(result);
} catch (error) {
    console.log(`Error: ${JSON.stringify(error)}`);
}
OUTPUT
Error: {"message": "promiseA something happened!"}

The result is the first fulfillment value. It contains a value of the method that will be the fastest. Promise.race() is fulfilled if the fastest promise is fulfilled and rejected when the fastest promise is rejected.

Promise.race - when to use?

This method is useful for returning the first promise that fulfills. It short-circuits after a promise fulfills, so it does not wait for the other promises to complete once it finds one. ~ [3] MDN Web Docs

Assume that you have configuration stored in three different systems Salesforce, AWS, and Firebase. It's important to you to load the page as soon as possible, so you call all three services but need only a response from the fastest one.

Promise.any vs Promise.race

Promise.any and Promise.race are quite similar, but do different things.

const promiseA = new Promise((resolve, reject) => setTimeout(() => reject('Oh something happened'), 200));

const promiseB = new Promise((resolve, reject) => setTimeout(() => resolve('It works!'), 300));

// Race vs Any - [promiseA, promiseB]

Promise.race([promiseA, promiseB])
    .then(result => {
        console.log(`race: success with: ${result}`);
    })
    .catch(error => {
         console.log(`race: error with: ${error}`);
    });

Promise.any([promiseA, promiseB])
    .then(result => {
        console.log(`any: success with: ${result}`);
    })
    .catch(error => {
         console.log(`any: error with: ${error}`);
    });

const promiseC = new Promise((resolve, reject) => setTimeout(() => reject('Oh something happend'), 400));

// Race vs Any - [promiseA, promiseC]

Promise.race([promiseA, promiseC])
    .then(result => {
        console.log(`race: success with: ${result}`);
    })
    .catch(error => {
         console.log(`race: error with: ${error}`);
    });

Promise.any([promiseA, promiseC])
    .then(result => {
        console.log(`any: success with: ${result}`);
    })
    .catch(error => {
         console.log(`any: error with: ${error}`);
    });
OUTPUT

Race vs Any - [promiseA, promiseB]
race: error with: Oh something happened
any: success with: It works!

Race vs Any - [promiseA, promiseC]
race: error with: Oh something happened
any: error with: AggregateError: All promises were rejected
promiseA => 200 ms => rejected
promiseB => 300 ms => fulfilled
promiseC => 400 ms => rejected

Explanation

Promise.race() is settled as soon as any of the passed promises is SETTLED (fulfilled or rejected).
In our example, Promise.race([promiseA, promiseB]), promiseA is settled (rejected) faster than promiseB.
The whole Promise.race() will be rejected and the code in the catch block will be executed.

Promise.any() is settled as soon as any of the passed promises is FULFILLED (success).

The only difference between Promise.race() and Promise.any() is that:

  • Promise.race() waiting for the first promise that will be SETTLED (fulfilled or rejected).
  • Promise.any() waiting for the first promise that will be FULFILLED (success), and rejects only when ALL of the passed promises are rejected.

Summary

Method Use Case When Rejects
Promise.all when multiple related promises must be completed at once and all need to be fulfilled (succeed) when ANY of the passed promises is rejected
Promise.allSettled when multiple NOT related promises must be completed at once NEVER
Promise.any when the result of the first promise that SETTLED (fulfilled or rejected) needed when ANY of the passed promises is rejected
Promise.race when the result of the first promise that FULFILLS (succeed) needed when ALL passed promises are rejected

If you have any questions feel free to ask in the comment section below. 🙂

Was it helpful? Check out our other great posts here.


Resources

Buy Me A Coffee