Introduction
Lightning Web Components (LWC) is a modern framework for building web components on the Salesforce platform. One of the most common tasks in LWC is making asynchronous calls to Apex methods to fetch or manipulate data.
In this blog post, we’ll explore how to handle asynchronous Apex calls in LWC, with detailed explanations and examples to help you understand the process.
What Are Asynchronous Apex Calls?
In simple terms, Asynchronous Apex calls are requests made to the server (Salesforce) that don’t block the user interface (UI) while waiting for a response.
Instead, the UI remains responsive, and the component reacts once the data is received or an error occurs. This is crucial for creating smooth and user-friendly applications.
Asynchronous operations in LWC occur when:
Calling Apex methods
Performing DML operations
Making API calls to external systems
Loading external resources
These operations don’t block the main thread, allowing your UI to remain responsive while waiting for responses.
Methods for Handling Asynchronous Apex Calls
1. Using @wire for Apex Calls
The @wire decorator provides a declarative way to call Apex methods. The Apex method is automatically called when the component loads or when the input parameters change.
Steps to Call Apex using Promises with @wire
Step 1: Create an Apex Class
// accountController.js (Apex)
public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List getAccounts() {
return [SELECT Id, Name, Industry, AnnualRevenue FROM Account ORDER BY Name LIMIT 10];
}
}
Here, getAccounts
 is an Apex method in the AccountController
 class.
Step 2: Use @wire to Call the Apex Method
Use the @wire decorator to call the Apex method and handle the response. But first, import the Apex method you want to call using the @salesforce/apex module.
// accountList.js
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
accounts;
error;
@wire(getAccounts)
wiredAccounts({ error, data }) {
if (data) {
this.accounts = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.accounts = undefined;
}
}
}
In this example:
TheÂ
@wire
 decorator calls theÂgetAccounts
 Apex method.The result is stored in theÂ
accounts
 property, which contains eitherÂdata
 orÂerror
.
Step 3: Display the Data in the Template
Use the data returned by the Apex method in your HTML template.
-
{account.Name} - {account.Industry}
Error loading accounts: {error.body.message}
Here:
If data is available, it displays a list of accounts.
If an error occurs, it shows the error message.
2. Using Imperative Apex Calls
Sometimes, you need to call an Apex method in response to a user action, such as clicking a button. This is called an imperative call.
Unlike @wire
, imperative calls give you more control over when the method is executed.
Steps to call Apex Classes Imperatively:
Step 1: Create the Apex Class
// contactController.js (Apex)
public with sharing class ContactController {
@AuraEnabled
public static Contact createContact(String firstName, String lastName, String email) {
Contact newContact = new Contact(
FirstName = firstName,
LastName = lastName,
Email = email
);
insert newContact;
return newContact;
}
}
Step 2: Call the Apex Method from LWC .js file
Use the imported method in your JavaScript code.
// createContact.js
import { LightningElement, track } from 'lwc';
import createContact from '@salesforce/apex/ContactController.createContact';
export default class CreateContact extends LightningElement {
@track firstName = '';
@track lastName = '';
@track email = '';
@track contactId;
@track error;
@track isLoading = false;
handleFirstNameChange(event) {
this.firstName = event.target.value;
}
handleLastNameChange(event) {
this.lastName = event.target.value;
}
handleEmailChange(event) {
this.email = event.target.value;
}
handleSubmit() {
this.isLoading = true;
createContact({
firstName: this.firstName,
lastName: this.lastName,
email: this.email
})
.then(result => {
this.contactId = result.Id;
this.error = undefined;
// Show success message
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Contact created successfully',
variant: 'success'
})
);
})
.catch(error => {
this.error = error;
this.contactId = undefined;
// Show error message
this.dispatchEvent(
new ShowToastEvent({
title: 'Error creating contact',
message: error.body.message,
variant: 'error'
})
);
})
.finally(() => {
this.isLoading = false;
});
}
}
Here:
TheÂ
saveAccount
 method is called when theÂhandleSave
 function is executed.If the call is successful, a success message is logged.
If an error occurs, the error message is logged.
Step 3: Trigger the Call
Call the Apex method in response to an event, such as a button click.
Contact created with ID: {contactId}
3. Chaining Multiple Apex Calls
When you need to make sequential calls where each depends on the previous one’s result.
Example: Creating an Opportunity for an Account
// opportunityController.js (Apex)
public with sharing class OpportunityController {
@AuraEnabled
public static Account getDefaultAccount() {
return [SELECT Id, Name FROM Account WHERE Name = 'Default Account' LIMIT 1];
}
@AuraEnabled
public static Opportunity createOpportunityForAccount(Id accountId, String oppName, Decimal amount) {
Opportunity newOpp = new Opportunity(
Name = oppName,
AccountId = accountId,
Amount = amount,
CloseDate = Date.today().addDays(30),
StageName = 'Prospecting'
);
insert newOpp;
return newOpp;
}
}
// createOpportunity.js
import { LightningElement, track } from 'lwc';
import getDefaultAccount from '@salesforce/apex/OpportunityController.getDefaultAccount';
import createOpportunityForAccount from '@salesforce/apex/OpportunityController.createOpportunityForAccount';
export default class CreateOpportunity extends LightningElement {
@track accountId;
@track oppName = '';
@track amount = 0;
@track opportunityId;
@track error;
@track isLoading = false;
connectedCallback() {
this.isLoading = true;
getDefaultAccount()
.then(result => {
this.accountId = result.Id;
this.error = undefined;
})
.catch(error => {
this.error = error;
this.accountId = undefined;
})
.finally(() => {
this.isLoading = false;
});
}
handleOppNameChange(event) {
this.oppName = event.target.value;
}
handleAmountChange(event) {
this.amount = event.target.value;
}
handleSubmit() {
if (!this.accountId) {
this.error = { message: 'Default account not found' };
return;
}
this.isLoading = true;
createOpportunityForAccount({
accountId: this.accountId,
oppName: this.oppName,
amount: this.amount
})
.then(result => {
this.opportunityId = result.Id;
this.error = undefined;
// Show success message
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Opportunity created successfully',
variant: 'success'
})
);
})
.catch(error => {
this.error = error;
this.opportunityId = undefined;
})
.finally(() => {
this.isLoading = false;
});
}
}
4. Parallel Apex Calls with Promise.all()
When you need to make multiple independent calls simultaneously.
Example: Fetching Dashboard Data
// dashboardController.js (Apex)
public with sharing class DashboardController {
@AuraEnabled(cacheable=true)
public static Integer getTotalAccounts() {
return [SELECT COUNT() FROM Account];
}
@AuraEnabled(cacheable=true)
public static Integer getTotalContacts() {
return [SELECT COUNT() FROM Contact];
}
@AuraEnabled(cacheable=true)
public static Integer getTotalOpportunities() {
return [SELECT COUNT() FROM Opportunity WHERE IsClosed = false];
}
}
// dashboardData.js
import { LightningElement, track } from 'lwc';
import getTotalAccounts from '@salesforce/apex/DashboardController.getTotalAccounts';
import getTotalContacts from '@salesforce/apex/DashboardController.getTotalContacts';
import getTotalOpportunities from '@salesforce/apex/DashboardController.getTotalOpportunities';
export default class DashboardData extends LightningElement {
@track accountsCount = 0;
@track contactsCount = 0;
@track opportunitiesCount = 0;
@track isLoading = true;
@track error;
connectedCallback() {
Promise.all([
getTotalAccounts(),
getTotalContacts(),
getTotalOpportunities()
])
.then(results => {
this.accountsCount = results[0];
this.contactsCount = results[1];
this.opportunitiesCount = results[2];
this.error = undefined;
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.isLoading = false;
});
}
}
Handling Errors while performing Apex calls
When working with asynchronous Apex calls, it’s important to handle errors gracefully. Both @wire
 and imperative calls provide ways to handle errors.
Using @wire:
TheÂ
error
 property is automatically populated if the Apex call fails.Display the error message in the UI or log it for debugging.
Error: {accounts.error.body.message}
Using Imperative Calls:
UseÂ
.catch()
 to handle errors in imperative calls.
saveAccount({ accountRecord: account })
.then(() => {
console.log('Account saved successfully');
})
.catch((error) => {
console.error('Error saving account:', error.body.message);
});
4. Best Practices for Asynchronous Apex Calls
1. Use cacheable=true Appropriately:
UseÂ
@AuraEnabled(cacheable=true)
 for Apex methods that retrieve data. This allows theÂ@wire
 decorator to cache the result and improve performance.
@AuraEnabled(cacheable=true)
public static List getAccountList() {
return [SELECT Id, Name FROM Account LIMIT 10];
}
2. Implement Proper Loading States:
Show a spinner or loading indicator while the Apex call is in progress.
Consider skeleton loaders for better perceived performance.
isLoading = false;
handleSave() {
this.isLoading = true;
saveAccount({ accountRecord: account })
.then(() => {
this.isLoading = false;
console.log('Account saved successfully');
})
.catch((error) => {
this.isLoading = false;
console.error('Error saving account:', error.body.message);
});
}
3. Debouncing
Use debouncing for search or filter inputs to avoid making too many Apex calls in a short time.
handleSearch(event) {
clearTimeout(this.delayTimeout);
const searchTerm = event.target.value;
this.delayTimeout = setTimeout(() => {
this.searchTerm = searchTerm;
}, 300);
}
4. Optimize Data Transfer:
Only request fields you need
Consider using custom Apex DTOs to reduce payload size
5. Error Handeling
Always include error handling in promises
Provide user-friendly error messages
Consider centralized error handling for consistency
6. Avoid Nested Promises
Use promise chaining or async/await instead of nesting
Consider Promise.all() for parallel independent operations
7. Memory Management:
Cancel pending promises when components are destroyed
Clean up event listeners in disconnectedCallback
Common Pitfalls and Solutions
Problem: Memory leaks from unresolved promises
Solution: Use AbortController or track promises to cancel them in disconnectedCallback
// abortControllerExample.js
import { LightningElement } from 'lwc';
import getData from '@salesforce/apex/MyController.getData';
export default class AbortExample extends LightningElement {
controller = new AbortController();
connectedCallback() {
getData({ signal: this.controller.signal })
.then(data => {
// Handle data
})
.catch(error => {
if (error.name !== 'AbortError') {
// Handle real errors
}
});
}
disconnectedCallback() {
this.controller.abort();
}
}
Problem: Unresponsive UI during multiple rapid requests
Solution: Implement debouncing for user-triggered actions
// debounceExample.js
import { LightningElement } from 'lwc';
import getSearchResults from '@salesforce/apex/SearchController.getSearchResults';
export default class DebounceExample extends LightningElement {
searchTerm = '';
timeoutId;
handleSearchChange(event) {
this.searchTerm = event.target.value;
// Clear previous timeout
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
// Set new timeout
this.timeoutId = setTimeout(() => {
this.performSearch();
}, 300); // 300ms delay
}
performSearch() {
getSearchResults({ searchTerm: this.searchTerm })
.then(results => {
// Process results
})
.catch(error => {
// Handle error
});
}
}
Example: Full Implementation
Here’s a complete example of handling asynchronous Apex calls in LWC:
Apex Controller:
public with sharing class AccountController {
@AuraEnabled(cacheable=true)
public static List getAccountList(String searchKey) {
String key = '%' + searchKey + '%';
return [SELECT Id, Name FROM Account WHERE Name LIKE :key LIMIT 10];
}
@AuraEnabled
public static void saveAccount(Account accountRecord) {
insert accountRecord;
}
}
LWC JavaScript:
import { LightningElement, wire, track } from 'lwc';
import getAccountList from '@salesforce/apex/AccountController.getAccountList';
import saveAccount from '@salesforce/apex/AccountController.saveAccount';
export default class AccountList extends LightningElement {
@track searchTerm = '';
@track accounts;
@track isLoading = false;
@wire(getAccountList, { searchKey: '$searchTerm' })
wiredAccounts({ data, error }) {
if (data) {
this.accounts = data;
} else if (error) {
console.error('Error fetching accounts:', error.body.message);
}
}
handleSearch(event) {
clearTimeout(this.delayTimeout);
const searchTerm = event.target.value;
this.delayTimeout = setTimeout(() => {
this.searchTerm = searchTerm;
}, 300);
}
handleSave() {
this.isLoading = true;
const account = { Name: 'New Account' };
saveAccount({ accountRecord: account })
.then(() => {
this.isLoading = false;
console.log('Account saved successfully');
})
.catch((error) => {
this.isLoading = false;
console.error('Error saving account:', error.body.message);
});
}
}
LWC HTML
{account.Name}
Conclusion
Handling asynchronous Apex calls in LWC is straightforward once you understand the concepts of @wire and imperative calls.
By following the steps and best practices outlined in this blog post, you can efficiently fetch and manipulate data in your Lightning Web Components while keeping the UI responsive and user-friendly.
Remember to:
Always handle loading and error states
Choose the right approach (wire vs imperative) for each use case
Optimize your Apex methods to return only necessary data
Implement proper cleanup to prevent memory leaks
Whether you’re building a simple list view or a complex form, mastering asynchronous Apex calls will help you create powerful and dynamic Salesforce applications.
Happy coding! 🚀