Pages

Thursday, May 5, 2022

Understanding of Async/Await in LWC - Extension to Promise




In the last post, we discussed about Promise function How to chain Asynchronous actions in LWC using Promise? While they provide easy control to Asynchronous JavaScript, but Async/Await which builds on Promises allowing them to be used in a way that much more closely resembles synchronous JavaScript.

Async

It is simply a keyword we put in front of a function definition. Async functions are instances of the AsyncFunction constructor.

async function performAction() {
   // do some asynchronous action
   return 'result of an action';
}

An Async function always returns a Promise. That promise will contain the value that you defined.

If you look at the below code script

function start() {
    console.log('start');
    console.log(performAction());
    console.log('stop');
}

start();

You might expect the below result

start
result of an action
stop

But in reality the result will be 

start
stop
result of an action

This is because performAction() was executed asynchronously.

Now, to make the above code work like synchronously or to execute all calls in sequential order we need to use Await function.

Await

The await expression causes the async function to pause until a Promise is returned for the async function. So, in simple words we are essentially telling our code to wait for the result of the asynchronous function to complete going any further.

await always expects a Promise from the async function, and parses the result automatically.

Lets consider the above code snippet

async function performAction() {
   // do some asynchronous action
   return 'result of an action';
}

async function start() {
    console.log('start');
    console.log(await performAction());
    console.log('stop');
}

start();

Now the result will be

start
result of an action
stop

This is because we are waiting the function performAction() to be completed before proceeding further.

NOTE: await can only be used within Async function.

Usage in LWC

Problem Statement : I need to re-render the validation message whenever the user checks Another amount as a radio button, but for me the DOM is not getting rendered properly on UI. So, I need to make a wait before I check for the selected radio button and checks the validity and reset it. So, to make that happen I need to put my change action in async/await.

Below is the code snippet:

payment.html

<!--
  @description       : 
  @author            : @tandonprateek
  @group             : 
  @last modified on  : 04-15-2022
  @last modified by  : @tandonprateek
-->
<template>
	<div class="slds-grid slds-gutters slds-var-m-bottom_small">
		<div class="slds-col slds-size_1-of-3 slds-nowrap">
			{labels.component.amountToPay}
		</div>
		<div class="slds-col">
			<lightning-radio-group 
                name="amountToPay"
                label={labels.component.amountToPay} 
                variant="label-hidden"
				options={amountToPayOptions} 
                value={amountToPaySelection} 
                type="radio" 
                onclick={handleAmountToPayChange}
				onchange={handleAmountToPayChange} 
                class="slds-var-m-bottom_small requiredInput" required>
			</lightning-radio-group>
			<lightning-input 
                type="number" 
                name="anotherAmount" 
                label="Another Amount" 
                variant="label-hidden"
				value={anotherAmount} 
                min="1" onfocus={handleAnotherAmountFocus} 
                onchange={handleAnotherAmountChange}
				formatter="currency" 
                class="inputAmount anotherAmount requiredInput" required={isAnotherAmountRequired}>
			</lightning-input>
		</div>
	</div>
</template>

payment.js

/**
 * @description       : 
 * @author            : @tandonprateek
 * @group             : 
 * @last modified on  : 05-05-2022
 * @last modified by  : @tandonprateek
**/
import { LightningElement } from 'lwc';

const ANOTHER_AMOUNT_OPTION = 'anotherAmount';
const AMOUNT_DUE_TODAY_OPTION = 'amountDueToday';


export default class Payment extends LightningElement {

    labels = {
        toast: {
            processingErrorTitle: 'Processing Error'
            , validationErrorTitle: 'Input Validation Error'
            , validationErrorMessage: 'Please provide values for all required fields'
        },
        component: {
            paymentHeader: 'Payment'
            , amountToPay: 'Amount to Pay'
            , amountDueToday: 'Amount Due Today '
            , remainingBalance: 'Remaining Balance '
            , anotherAmount: 'Another Amount'
        }
    }

    amountToPaySelection;
    anotherAmount;
    isAnotherAmountRequired = false;

    connectedCallback() {
        this.amountToPaySelection = AMOUNT_DUE_TODAY_OPTION
    }

    /**
        This method will wait for the Promise to be resolved and will then reset the validity.
    **/
    async handleAmountToPayChange(event) {
        this.amountToPaySelection = event.detail.value;
        const anotherAmountField = this.template.querySelector('.anotherAmount');
        if(this.amountToPaySelection !== ANOTHER_AMOUNT_OPTION) {
            this.anotherAmount = undefined;
            await Promise.resolve();
            anotherAmountField.reportValidity();
        } else {
          anotherAmountField.focus();
          anotherAmountField.reportValidity();
        }
        this.isAnotherAmountRequired = this.amountToPaySelection === ANOTHER_AMOUNT_OPTION;
    }
    
    handleAnotherAmountChange(event) {
        const value = event.detail.value;
        this.anotherAmount = value;
    }
    
    handleAnotherAmountFocus(event) {
        this.amountToPaySelection = ANOTHER_AMOUNT_OPTION;
        this.isAnotherAmountRequired = this.amountToPaySelection === ANOTHER_AMOUNT_OPTION;
    }
    
    get amountToPayOptions() {
        const theOptions = [];
        theOptions.push({ label: this.labels.component.amountDueToday, value: AMOUNT_DUE_TODAY_OPTION});
    
        let balanceLabel = this.labels.component.remainingBalance;
    
        theOptions.push({ label: balanceLabel, value: 'remainingBalance'});
        
        theOptions.push({ label: this.labels.component.anotherAmount, value: ANOTHER_AMOUNT_OPTION });
    
        return theOptions;
    }

}




Error Handling 

To handle error we will be using old try/catch block.

async function doSomething() {
   // do something asyncronous
   return 'something';
}

async function start() {
    try {
        console.log('start');
        console.log(await performAction());
        console.log('end');
    } catch(e) {
        console.error(e);
    } finally {
        console.log('Do something no matter what');
    }
}

start();