Mastering Async/Await in JavaScript and Salesforce LWC

Introduction

JavaScript has evolved significantly over the years, particularly in how it handles asynchronous operations.

One of the most powerful additions to the language has been the async/await syntax, which has transformed how developers write asynchronous code.

Understanding Asynchronous JavaScript

Before we discuss async/await, let’s briefly understand why asynchronous programming is essential in JavaScript.

JavaScript is single-threaded, meaning it can only execute one command at a time. However, operations like API calls, database queries, or file I/O take time to complete. If JavaScript were to wait for these operations to finish before moving on, the application would freeze, creating a poor user experience.

To solve this problem, JavaScript uses asynchronous programming patterns:

  1. Callbacks: Functions passed as arguments to be executed after an operation completes
  2. Promises: Objects representing the eventual completion or failure of an asynchronous operation
  3. Async/Await: Syntactic sugar built on top of Promises, making asynchronous code more readable

Promises: The Foundation

Promises were introduced to address “callback hell” – deeply nested callbacks that make code difficult to read and maintain. A Promise represents an operation that hasn’t completed yet but is expected to in the future.

A Promise can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: Operation completed successfully
  • Rejected: Operation failed

Here’s a basic Promise example:

const fetchData = () => {
  return new Promise((resolve, reject) => {
    // Simulating an API call
    setTimeout(() => {
      const data = { name: "John", age: 30 };
      // Operation successful
      resolve(data);
      // If operation fails:
      // reject("Error fetching data");
    }, 2000);
  });
};

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

Enter Async/Await

While Promises improved code readability compared to callbacks, they still required chaining .then() and .catch() methods, which could become complex with multiple asynchronous operations.

async/await was introduced in ES2017 (ES8) as a more elegant way to work with Promises. It allows you to write asynchronous code that looks synchronous, making it more readable and maintainable.

The Basics of Async/Await

  1. async: When you add the async keyword before a function declaration, it becomes an async function that automatically returns a Promise.
  2. await: The await keyword can only be used inside an async function. It pauses the execution of the function until the Promise is resolved, and then returns the resolved value.

Here’s our previous example rewritten with async/await:

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { name: "John", age: 30 };
      resolve(data);
    }, 2000);
  });
};

const getData = async () => {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};

getData();

Benefits of Async/Await

  1. Readability: Code flows like synchronous code, making it easier to understand
  2. Error handling: You can use standard try/catch blocks
  3. Debugging: Easier to set breakpoints and step through code
  4. Sequential execution: Simplified way to execute asynchronous operations in sequence

Async/Await in Salesforce Lightning Web Components (LWC)

Salesforce Lightning Web Components framework fully supports async/await, making it an excellent choice for handling server-side interactions through Apex methods.

Basic Structure of LWC-Apex Interaction

In Salesforce LWC, you typically use the @wire adapter or the imperative approach to call Apex methods. Let’s focus on the imperative approach, where async/await is particularly useful.

  1. First, you create an Apex class with methods annotated with @AuraEnabled:
public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> getContacts() {
        return [SELECT Id, Name, Email FROM Contact LIMIT 10];
    }
}
  1. Then, you import and use this method in your LWC JavaScript file:
import { LightningElement } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';

export default class ContactList extends LightningElement {
    contacts;
    error;

    async connectedCallback() {
        try {
            this.contacts = await getContacts();
        } catch (error) {
            this.error = error;
        }
    }
}

Error Handling with Async/Await in LWC

Error handling becomes more intuitive with async/await. Simply wrap your code in a try/catch block:

async handleClick() {
    try {
        this.loading = true;
        const result = await serverOperation();
        this.data = result;
        this.loading = false;
    } catch (error) {
        this.error = error.message || 'Unknown error occurred';
        this.loading = false;
    }
}

Advanced Patterns with Async/Await in Salesforce LWC

Sequential Apex Calls

Sometimes you need to make sequential Apex calls where each call depends on the result of the previous one:

import { LightningElement } from 'lwc';
import getAccount from '@salesforce/apex/DataService.getAccount';
import getContactsByAccountId from '@salesforce/apex/DataService.getContactsByAccountId';
import getOpportunitiesByAccountId from '@salesforce/apex/DataService.getOpportunitiesByAccountId';

export default class SequentialCalls extends LightningElement {
    accountData;
    contactsData;
    opportunitiesData;
    error;
    
    async fetchAccountData() {
        try {
            // First call
            const account = await getAccount({ accountName: 'Acme Corp' });
            this.accountData = account;
            
            if (account && account.Id) {
                // Second call using result from first call
                this.contactsData = await getContactsByAccountId({ accountId: account.Id });
                
                // Third call using result from first call
                this.opportunitiesData = await getOpportunitiesByAccountId({ accountId: account.Id });
            }
        } catch (error) {
            this.error = error.message || 'An error occurred';
        }
    }
}

Parallel Apex Calls with Promise.all()

When multiple Apex calls are independent of each other, you can run them in parallel using Promise.all() with async/await:

import { LightningElement } from 'lwc';
import getAccounts from '@salesforce/apex/DataService.getAccounts';
import getContacts from '@salesforce/apex/DataService.getContacts';
import getOpportunities from '@salesforce/apex/DataService.getOpportunities';

export default class ParallelCalls extends LightningElement {
    accounts;
    contacts;
    opportunities;
    error;
    loading = false;
    
    async fetchAllData() {
        this.loading = true;
        try {
            // Run all three calls in parallel
            const [accountsResult, contactsResult, opportunitiesResult] = await Promise.all([
                getAccounts(),
                getContacts(),
                getOpportunities()
            ]);
            
            // Set all results at once
            this.accounts = accountsResult;
            this.contacts = contactsResult;
            this.opportunities = opportunitiesResult;
        } catch (error) {
            this.error = error.message || 'An error occurred';
        } finally {
            this.loading = false;
        }
    }
}

Race Conditions with Promise.race()

Sometimes you might want to use the result of whichever Apex call resolves first:

import { LightningElement } from 'lwc';
import getDataFromPrimarySource from '@salesforce/apex/DataService.getDataFromPrimarySource';
import getDataFromBackupSource from '@salesforce/apex/DataService.getDataFromBackupSource';

export default class RaceCall extends LightningElement {
    data;
    error;
    
    async fetchDataFromFastestSource() {
        try {
            // Use whichever call resolves first
            this.data = await Promise.race([
                getDataFromPrimarySource(),
                getDataFromBackupSource()
            ]);
        } catch (error) {
            this.error = error.message || 'Both data sources failed';
        }
    }
}

Practical Example: Complex Component Using Multiple Apex Calls

Let’s build a practical example of an Account Dashboard component that fetches account details, recent contacts, and opportunities using multiple Apex calls:

Apex Controller

public with sharing class AccountDashboardController {
    @AuraEnabled
    public static Account getAccountDetails(String accountId) {
        return [SELECT Id, Name, BillingAddress, Phone, Website, Industry 
                FROM Account WHERE Id = :accountId LIMIT 1];
    }
    
    @AuraEnabled
    public static List<Contact> getRecentContacts(String accountId) {
        return [SELECT Id, Name, Email, Phone, Title 
                FROM Contact 
                WHERE AccountId = :accountId 
                ORDER BY CreatedDate DESC LIMIT 5];
    }
    
    @AuraEnabled
    public static List<Opportunity> getOpenOpportunities(String accountId) {
        return [SELECT Id, Name, Amount, CloseDate, StageName 
                FROM Opportunity 
                WHERE AccountId = :accountId AND IsClosed = false 
                ORDER BY CloseDate ASC LIMIT 5];
    }
    
    @AuraEnabled
    public static Map<String, Object> getAccountSummary(String accountId) {
        Map<String, Object> result = new Map<String, Object>();
        
        // Aggregate query for total opportunity amount
        AggregateResult[] oppAmounts = [
            SELECT SUM(Amount) totalAmount 
            FROM Opportunity 
            WHERE AccountId = :accountId
        ];
        
        // Count of related cases
        Integer caseCount = [
            SELECT COUNT() 
            FROM Case 
            WHERE AccountId = :accountId
        ];
        
        result.put('totalOpportunityAmount', oppAmounts[0].get('totalAmount'));
        result.put('openCaseCount', caseCount);
        
        return result;
    }
}

LWC JavaScript (with async/await)

import { LightningElement, api, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccountDetails from '@salesforce/apex/AccountDashboardController.getAccountDetails';
import getRecentContacts from '@salesforce/apex/AccountDashboardController.getRecentContacts';
import getOpenOpportunities from '@salesforce/apex/AccountDashboardController.getOpenOpportunities';
import getAccountSummary from '@salesforce/apex/AccountDashboardController.getAccountSummary';

export default class AccountDashboard extends LightningElement {
    @api recordId; // Account Id passed from record page
    
    account;
    contacts;
    opportunities;
    summary;
    
    loading = true;
    error;
    
    // Load all data when component initializes
    async connectedCallback() {
        if (this.recordId) {
            await this.loadAllData();
        }
    }
    
    // Method to handle refresh button click
    async handleRefresh() {
        this.loading = true;
        this.error = null;
        await this.loadAllData();
    }
    
    // Load all data in parallel for maximum efficiency
    async loadAllData() {
        try {
            const [accountResult, contactsResult, opportunitiesResult, summaryResult] = await Promise.all([
                getAccountDetails({ accountId: this.recordId }),
                getRecentContacts({ accountId: this.recordId }),
                getOpenOpportunities({ accountId: this.recordId }),
                getAccountSummary({ accountId: this.recordId })
            ]);
            
            this.account = accountResult;
            this.contacts = contactsResult;
            this.opportunities = opportunitiesResult;
            this.summary = summaryResult;
            
            this.showToast('Success', 'Account data loaded successfully', 'success');
        } catch (error) {
            this.error = error.message || 'Unknown error loading account data';
            this.showToast('Error', this.error, 'error');
        } finally {
            this.loading = false;
        }
    }
    
    // Sequential loading example (not used but shown for demonstration)
    async loadDataSequentially() {
        try {
            // First load account
            this.account = await getAccountDetails({ accountId: this.recordId });
            
            // Then load contacts and opportunities in parallel
            [this.contacts, this.opportunities] = await Promise.all([
                getRecentContacts({ accountId: this.recordId }),
                getOpenOpportunities({ accountId: this.recordId })
            ]);
            
            // Finally load summary
            this.summary = await getAccountSummary({ accountId: this.recordId });
        } catch (error) {
            this.error = error.message || 'Unknown error loading account data';
        } finally {
            this.loading = false;
        }
    }
    
    showToast(title, message, variant) {
        this.dispatchEvent(
            new ShowToastEvent({
                title,
                message,
                variant
            })
        );
    }
}

LWC HTML Template

<template>
    <lightning-card title="Account Dashboard" icon-name="standard:account">
        <div class="slds-var-p-around_medium">
            <!-- Loading spinner -->
            <template if:true={loading}>
                <lightning-spinner alternative-text="Loading data" size="medium"></lightning-spinner>
            </template>
            
            <!-- Error message -->
            <template if:true={error}>
                <div class="slds-text-color_error slds-var-p-around_small">
                    {error}
                </div>
            </template>
            
            <!-- Account details -->
            <template if:true={account}>
                <div class="slds-grid slds-gutters slds-wrap">
                    <!-- Account information -->
                    <div class="slds-col slds-size_1-of-1 slds-large-size_1-of-2">
                        <div class="slds-text-heading_medium slds-var-m-bottom_small">
                            {account.Name}
                        </div>
                        <div class="slds-var-m-bottom_small">
                            <div><strong>Industry:</strong> {account.Industry}</div>
                            <div><strong>Phone:</strong> {account.Phone}</div>
                            <div><strong>Website:</strong> {account.Website}</div>
                        </div>
                    </div>
                    
                    <!-- Account summary -->
                    <div class="slds-col slds-size_1-of-1 slds-large-size_1-of-2">
                        <template if:true={summary}>
                            <div class="slds-text-heading_medium slds-var-m-bottom_small">
                                Summary
                            </div>
                            <div class="slds-var-m-bottom_small">
                                <div><strong>Total Opportunity Amount:</strong> 
                                    <lightning-formatted-number value={summary.totalOpportunityAmount} 
                                                             format-style="currency" 
                                                             currency-code="USD">
                                    </lightning-formatted-number>
                                </div>
                                <div><strong>Open Cases:</strong> {summary.openCaseCount}</div>
                            </div>
                        </template>
                    </div>
                </div>
                
                <!-- Recent contacts -->
                <div class="slds-var-m-top_medium">
                    <div class="slds-text-heading_medium slds-var-m-bottom_small">
                        Recent Contacts
                    </div>
                    <template if:true={contacts}>
                        <lightning-datatable
                            key-field="Id"
                            data={contacts}
                            columns={contactColumns}
                            hide-checkbox-column>
                        </lightning-datatable>
                    </template>
                </div>
                
                <!-- Open opportunities -->
                <div class="slds-var-m-top_medium">
                    <div class="slds-text-heading_medium slds-var-m-bottom_small">
                        Open Opportunities
                    </div>
                    <template if:true={opportunities}>
                        <lightning-datatable
                            key-field="Id"
                            data={opportunities}
                            columns={opportunityColumns}
                            hide-checkbox-column>
                        </lightning-datatable>
                    </template>
                </div>
            </template>
            
            <!-- Refresh button -->
            <div class="slds-var-m-top_medium slds-align_absolute-center">
                <lightning-button 
                    label="Refresh Data" 
                    icon-name="utility:refresh" 
                    onclick={handleRefresh}
                    disabled={loading}>
                </lightning-button>
            </div>
        </div>
    </lightning-card>
</template>

Additional JavaScript for Datatable Columns

// Add these properties to the JavaScript class
get contactColumns() {
    return [
        { label: 'Name', fieldName: 'Name', type: 'text' },
        { label: 'Title', fieldName: 'Title', type: 'text' },
        { label: 'Email', fieldName: 'Email', type: 'email' },
        { label: 'Phone', fieldName: 'Phone', type: 'phone' }
    ];
}

get opportunityColumns() {
    return [
        { label: 'Name', fieldName: 'Name', type: 'text' },
        { label: 'Amount', fieldName: 'Amount', type: 'currency' },
        { label: 'Close Date', fieldName: 'CloseDate', type: 'date' },
        { label: 'Stage', fieldName: 'StageName', type: 'text' }
    ];
}

Best Practices for Using Async/Await in Salesforce LWC

  1. Always handle errors: Use try/catch blocks to gracefully handle exceptions.
  2. Show loading states: Let users know when asynchronous operations are in progress.
  3. Use Promise.all() for independent calls: When multiple Apex calls don’t depend on each other, run them in parallel.
  4. Use sequential calls when necessary: If one call depends on the result of another, structure your code accordingly.
  5. Keep component state consistent: Update component properties atomically to avoid inconsistent UI states.
  6. Add timeouts when appropriate: For critical operations, consider adding timeouts using Promise.race().
  7. Use finally blocks: Ensure cleanup code runs regardless of success or failure.
  8. Consider caching results: Use the cacheable=true parameter on @AuraEnabled methods when appropriate.
  9. Separate business logic: Move complex async logic to separate methods for better maintainability.
  10. Consider performance impact: Be mindful of making too many server calls, especially in loops.

Conclusion

Async/await has revolutionized how we write asynchronous JavaScript code, making it more readable, maintainable, and error-resistant. In Salesforce LWC, this pattern is particularly powerful for handling Apex method calls and creating responsive, user-friendly interfaces.

Must Read

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading