LWC and async functions
--
What a lovely day in The Netherlands. You wake up thinking that it will be a sunny day, but 20 minutes later it starts to rain.
It’s that time of the year when it rains every day and you always need to go outside with a raincoat and your eyes on the rain radar app that it’s installed on your smartphone. Ah, I am missing already those days when the sunset happens at 16:00. Winter is coming 😢.
Anyways, today I am going to write about Javascript async functions. Here is a quick definition from the MDN website:
An async function is a function declared with the
async
keyword. Async functions are instances of theAsyncFunction
constructor, and theawait
keyword is permitted within them. Theasync
andawait
keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
In my last post, I’ve described how to use promises and especially how to chain them by getting the result of one and use it as a parameter for the next ones. As you could read from de MDN definition of async functions quoted above, it’s much more clear to write your asynchronous calls using async functions.
To explain the async behavior, I am going to use the same example that I used in my previous post. The idea is to create one account record and call an apex method to integrate this newly created account to an external system. Let’s start with the HMTL part of the component:
<template>
<lightning-card icon-name="action:new_account" title="Create Account">
<div slot="actions">
<lightning-button
class="slds-m-top_small"
onclick={handleCreate}
label="Create">
</lightning-button>
</div>
<div class="slds-p-around_small">
<lightning-layout multiple-rows>
<lightning-layout-item size="12">
<lightning-input type="text"
label="Account Name"
name="accountName"
required
onchange={handleChange}
value={accountName}></lightning-input>
</lightning-layout-item>
</lightning-layout>
</div>
</lightning-card>
</template>
And the Javascript controller below. For now, we are just creating the account:
import {LightningElement} from 'lwc';
import { createRecord } from 'lightning/uiRecordApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
export default class CreateAccountAsync extends LightningElement {
accountName;
isLoading;
async handleCreate(){
const inputFields = this.template.querySelectorAll('lightning-input');
const allValid = this.checkFieldsValidity(inputFields);
if(allValid){
this.isLoading = true;
const fields = {};
fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.accountName;
const accountInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
try{
const createRecordResult = await createRecord(accountInput);
console.log('account created with id: ', createRecordResult.id);
}catch (error) {
console.log('error', error);
}
this.isLoading = false;
}
}
checkFieldsValidity(fields){
const allValid = [...fields].reduce((validSoFar, field) => {
return validSoFar && field.reportValidity();
}, true);
return allValid;
}
handleChange(event){
if(event.target.name === "accountName"){
this.accountName = event.target.value;
}
}
}
Let’s check the handleCreate function. But first, let’s see how we did it using promises:
const fields = {};
fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.accountName;
const accountInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };createRecord(accountInput)
.then(result => {
console.log('account created with id: ', result.id);
})
.catch(error => {
console.log('error', error);
});
And the newer version of the code using async:
const fields = {};
fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.accountName;
const accountInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };try{
const createRecordResult = await createRecord(accountInput);
console.log('account created with id: ', createRecordResult.id);
}catch (error) {
console.log('error', error);
}
this.isLoading = false;
Note that there are small, but noticeable changes here. First, we don’t need to use the .then(). Instead, we use the await keyword to tell the Javascript engine that we want to wait for that function (createRecord) to be executed before executing the subsequent lines. If the createRecord is called without the await keyword, the console.log will be executed right away and we would get a log like this: account created with id: undefined. And remember, you need to annotate your function with the async word, like this:
async handleCreate(){
...
}
The second thing that I would like to give attention to is how we handle errors using async/await. With the promises way of calling functions, we would use the .catch() function to handle the errors. When it comes to async/await, it’s necessary to use the try/catch blocks:
try{
const createRecordResult = await createRecord(accountInput);
console.log('account created with id: ', createRecordResult.id);
}catch (error) {
console.log('error', error);
}
Now, let’s include the method to integrate the account with another system. Again, as in my last post, I am not going to really make a callout, but use an Apex method that waits a few seconds before returning something to the LWC:
@AuraEnabled
public static String integrateAccount(String accountId){
//do your API call here.
Datetime targetDate = System.now().addSeconds(5);
while(System.Now() < targetDate){
}
return 'ok';
}
Going back to the LWC, let’s import the Apex method:
import integrateAccount from '@salesforce/apex/createAccountController.integrateAccount';
And change the handleCreate function to also call the newly imported integrateAccount function when the account is created. The lines in bold are the ones that were added:
const fields = {};
fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.accountName;
const accountInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
try{
const createRecordResult = await createRecord(accountInput);
console.log('account created with id: ', createRecordResult.id);
const integrationResult = await integrateAccount({ accountId: createRecordResult.id });
console.log('success!', integrationResult);
}catch (error) {
console.log('error', error);
}
Now we have the two asynchronous functions within the same block. The integrateAccount will not be executed before the result of the createRecord is in place. And we could even add more functions to be called using the await keyword, depending on our business scenario and what’s is necessary to achieve.
Here is the final version of the js controller:
import {LightningElement} from 'lwc';
import { createRecord } from 'lightning/uiRecordApi';
import integrateAccount from '@salesforce/apex/createAccountController.integrateAccount';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
export default class CreateAccountAsync extends LightningElement {
accountName;
isLoading;
async handleCreate(){
const inputFields = this.template.querySelectorAll('lightning-input');
const allValid = this.checkFieldsValidity(inputFields);
if(allValid){
this.isLoading = true;
const fields = {};
fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.accountName;
const accountInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
try{
const createRecordResult = await createRecord(accountInput);
console.log('account created with id: ', createRecordResult.id);
const integrationResult = await integrateAccount({ accountId: createRecordResult.id });
console.log('success!', integrationResult);
}catch (error) {
console.log('error', error);
}
this.isLoading = false;
}
}
checkFieldsValidity(fields){
const allValid = [...fields].reduce((validSoFar, field) => {
return validSoFar && field.reportValidity();
}, true);
return allValid;
}
handleChange(event){
if(event.target.name === "accountName"){
this.accountName = event.target.value;
}
}
}
I hope you enjoyed this post and that it could help you in the future. If you have any questions/something to add, just leave a comment here. Enjoy your week and for the ones in The Netherlands, don’t forget your raincoat at home!