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.
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:
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:

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));
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.
async keyword before a function declaration, it becomes an async function that automatically returns a Promise.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();
Salesforce Lightning Web Components framework fully supports async/await, making it an excellent choice for handling server-side interactions through Apex methods.
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.

@AuraEnabled:public with sharing class ContactController {
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts() {
return [SELECT Id, Name, Email FROM Contact LIMIT 10];
}
}
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 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;
}
}
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';
}
}
}
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;
}
}
}
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';
}
}
}
Let’s build a practical example of an Account Dashboard component that fetches account details, recent contacts, and opportunities using multiple Apex calls:

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;
}
}
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
})
);
}
}
<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>
// 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' }
];
}
Promise.race().cacheable=true parameter on @AuraEnabled methods when appropriate.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.