In this article, we’ll initially explore Promises from a JavaScript perspective, and subsequently, with an example component, we’ll examine them within the context of Lightning Web Components LWC
A Promise is an entity representing the eventual accomplishment or failure of an asynchronous task. Since many individuals utilize pre-existing promises, this guide will elucidate the handling of returned promises before detailing how to construct them.
In essence, a promise is a returned entity to which you bind callbacks, rather than providing callbacks directly into a function.
Consider a function, createAudioFileAsync(), which asynchronously produces a sound file based on a configuration record, along with two callback functions—one triggered upon successful creation of the audio file, and the other invoked in the event of an error.
Below is some code employing createAudioFileAsync():
function successCallback(result) { console.log("Audio file ready at URL: " + result); } function failureCallback(error) { console.error("Error generating audio file: " + error); } createAudioFileAsync(audioSettings, successCallback, failureCallback);
With contemporary functions that return a promise, you can instead attach your callbacks to them:
If createAudioFileAsync() were refactored to return a promise, utilizing it could be as straightforward as follows:
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
That’s shorthand for:
const promise = createAudioFileAsync(audioSettings); promise.then(successCallback, failureCallback);
This is referred to as an asynchronous function call, which offers several advantages. Let’s delve into each one.
Assurances
Unlike traditional callbacks passed directly, a promise provides certain assurances:
Callbacks won’t be invoked before the current iteration of the JavaScript event loop completes. Callbacks appended with then() will still be executed even after the asynchronous operation has succeeded or failed, as mentioned earlier. Multiple callbacks can be appended by invoking then() multiple times. Each callback is executed sequentially, following the order of insertion. One of the notable advantages of using promises is their capability for chaining.
Chaining
A prevalent requirement is to consecutively execute two or more asynchronous operations, where each subsequent operation begins once the preceding one succeeds, utilizing the result from the prior step. This is achieved through the creation of a promise chain.
Here’s the magic: the then()
function returns a new promise, different from the original:
const promise = doSomething(); const promise2 = promise.then(successCallback, failureCallback);
or
const promise2 = doSomething().then(successCallback, failureCallback);
This subsequent promise (promise2) signifies the culmination not only of doSomething() but also of the successCallback or failureCallback you provided, which could be other asynchronous functions returning a promise. In such instances, any callbacks attached to promise2 are queued behind the promise returned by either successCallback or failureCallback.
Essentially, each promise symbolizes the conclusion of another asynchronous step within the sequence.
In the past, performing multiple asynchronous operations sequentially would result in the traditional callback pyramid of doom:
doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('Got the final result: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback);
With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:
doSomething() .then(function(result) { return doSomethingElse(result); }) .then(function(newResult) { return doThirdThing(newResult); }) .then(function(finalResult) { console.log('Got the final result: ' + finalResult); }) .catch(failureCallback);
The parameters for then are not mandatory, and catch(failureCallback) is a shorthand for then(null, failureCallback). This could also be represented using arrow functions:
doSomething() .then(result => doSomethingElse(result)) .then(newResult => doThirdThing(newResult)) .then(finalResult => { console.log(`Got the final result: ${finalResult}`); }) .catch(failureCallback);
Important: Always return results, otherwise callbacks won’t catch the result of a previous promise (with arrow functions () => x
is short for () => { return x; }
).
Asynchronous Component in LWC
In this demonstration, I’ll present a reusable component that allows developers to display one of three components depending on the status of the fetched data.
The component is crafted using Salesforce’s recently introduced Lightning Web Components (LWC) framework, which represents a fresh programming model for constructing Lightning components. It capitalizes on the advancements in web standards over the past five years, while introducing contemporary JavaScript syntax to front-end development within the Salesforce ecosystem.
The scenario involves a component that accepts a promise as an attribute and dynamically renders a component based on the promise’s status. Promises have been gradually replacing callbacks for handling asynchronous code in the JavaScript landscape, and LWC fully embraces this paradigm. In our example, we’ll retrieve a list of contacts from the server using Apex. If the server successfully returns the data, the promise resolves (is fulfilled), and the component displays the names of the contacts. Conversely, if an error occurs, the promise is rejected, and the component exhibits an error message. While the data is being retrieved (i.e., while the promise is pending), the component indicates this state by displaying a loading spinner.
The primary objective here is to decouple the display logic from the component responsible for fetching the data. The component responsible for data retrieval knows the nature of the data and determines what should be displayed for each possible state of the promise (fulfilled, rejected, or pending). However, given that rendering various components based on asynchronous data is a common pattern, I’ve separated this concern into a distinct component named “async”. Below, you’ll find the HTML and JavaScript code for this “async” component.
<template> <div> <template if:true={value}> <slot name="onFulfilled"></slot> </template> <template if:true={error}> <slot name="onRejected"></slot> </template> <template if:true={pending}> <slot name="onPending"></slot> </template> <slot></slot> </div> </template> async.html
import { LightningElement, track, api } from 'lwc'; export default class Async extends LightningElement { @track value; @track error; @track pending = true; @api promise; connectedCallback() { this.promise .then(value => { this.value = value }) .catch(error => { this.error = error }) .finally(() => { this.pending = false }); } } async.js
The async component does not determine what to display for each promise state. Its role is simply to receive a promise as an attribute and include the markup for the body it should render within named slots. It leverages slots and templates, which are not exclusive to LWC but are rather part of the Web Components standard.
Utilizing the Async Component
I’ve developed another component named contactList, which employs the async component to fulfill the previously described use case. Below is the markup and JavaScript for this component.
<template> <lightning-card title="Contact List" icon-name="standard:contact"> <div class="slds-m-around_medium"> <c-async promise={contactPromise}> <div slot="onFulfilled"> <template if:true={contacts}> <template for:each={contacts} for:item="contact"> <p key={contact.Id}>{contact.Name}</p> </template> </template> </div> <div slot="onRejected"> Error occured while retrieving Contacts. </div> <div slot="onPending"> <lightning-spinner alternative-text="Loading"></lightning-spinner> </div> </c-async> </div> </lightning-card> </template> contactList.html
import { LightningElement, track } from 'lwc'; import getContactList from '@salesforce/apex/ContactController.getContactList'; export default class ContactList extends LightningElement { @track contacts contactPromise = getContactList(); connectedCallback() { getContactList().then(result => { this.contacts = result; }) } } contactList.js
public with sharing class ContactController { @AuraEnabled public static List<Contact> getContactList() { return [SELECT Id, Name, Title, Phone, Email FROM Contact LIMIT 10]; } } ContactController.cls
The contactList component invokes an Apex method on the server, which returns a promise. This promise is subsequently passed into the async component. It’s noteworthy how Salesforce has streamlined the syntax for making server calls. Additionally, LWC components, unlike Aura components in the past, are not bound to a single server-side controller; they can import any Aura-enabled Apex method.