SfSkills Domains Docs GitHub

Home / lwc / lwc-async-patterns

lwc-async-patterns

Async/await and Promise patterns in LWC: imperative Apex, loading states, error handling, concurrent wire + imperative, AbortController for in-flight cancellation, Promise.all for parallel calls. NOT for the basic imperative Apex import-and-call pattern or cacheable=true rules — use lwc/lwc-imperative-apex. NOT for @wire provisioning basics — use lwc/wire-service-patterns.

Open the skill folder on GitHub


LWC Async Patterns

Activate when writing asynchronous logic in LWC — imperative Apex calls, fetch to external endpoints, or complex orchestration of multiple data sources. LWC supports async/await natively; getting loading states, error handling, and cancellation right separates a polished component from a flaky one.

Before Starting

Core Concepts

Imperative Apex with async/await

import getAccounts from '@salesforce/apex/AccountService.getAccounts';

async handleLoad() {
    this.isLoading = true;
    try {
        this.accounts = await getAccounts({ region: this.region });
    } catch (err) {
        this.error = err.body?.message ?? 'Unknown error';
    } finally {
        this.isLoading = false;
    }
}

Parallel calls

const [accs, cons, opps] = await Promise.all([
    getAccounts(),
    getContacts(),
    getOpportunities()
]);

Fails fast — first rejection aborts. Use Promise.allSettled if partial results are acceptable.

AbortController for cancellation

this.controller?.abort();  // cancel prior
this.controller = new AbortController();

try {
    const resp = await fetch(url, { signal: this.controller.signal });
    // process
} catch (e) {
    if (e.name === 'AbortError') return;  // expected cancellation
    throw e;
}

Race-condition guard: stale responses

If the user changes filter while a call is in flight, a late response can overwrite fresh state. Track a request Id:

const reqId = ++this.latestReqId;
const data = await getData(...);
if (reqId !== this.latestReqId) return;  // stale
this.data = data;

Loading + error states

@track isLoading = false;
@track error;
@track data;

Template:

<template if:true={isLoading}><lightning-spinner /></template>
<template if:true={error}><c-error-banner message={error} /></template>
<template if:true={data}><!-- content --></template>

Common Patterns

Pattern: Debounced search with cancellation

handleSearch(e) {
    clearTimeout(this.timer);
    this.timer = setTimeout(async () => {
        this.controller?.abort();
        this.controller = new AbortController();
        try {
            this.results = await searchApex({ q: e.target.value });
        } catch (err) {
            if (err.name !== 'AbortError') this.error = err;
        }
    }, 300);
}

Pattern: Retry with exponential backoff

async fetchWithRetry(fn, retries = 3, delay = 500) {
    for (let i = 0; i < retries; i++) {
        try { return await fn(); }
        catch (e) {
            if (i === retries - 1) throw e;
            await new Promise(r => setTimeout(r, delay * 2 ** i));
        }
    }
}

Pattern: Dependent chained calls

const user = await getUser();
const [orders, tickets] = await Promise.all([
    getOrders(user.id),
    getTickets(user.id)
]);

Decision Guidance

SituationApproach
Reactive data that updates with context@wire
On-demand fetch (button click)imperative + async/await
Multiple independent callsPromise.all
Partial-success acceptablePromise.allSettled
User can navigate away mid-fetchAbortController
User rapidly retypes a querydebounce + cancel prior
  1. Classify each call as wire or imperative.
  2. For imperative, wrap in try/catch with finally resetting loading state.
  3. For multiple calls, use Promise.all when order doesn't matter.
  4. Add AbortController or request-id guards to prevent stale updates.
  5. Surface errors via ShowToastEvent (sticky for errors) OR inline banner.
  6. Test with slow-network throttling in browser devtools.
  7. Ensure unmount path clears pending timers / aborts controllers.

Review Checklist

Salesforce-Specific Gotchas

  1. Imperative Apex errors have shape {body: {message}} — not .message directly. Always err.body?.message.
  2. LWC does NOT expose a componentWillUnmount lifecycle hook. Use disconnectedCallback to abort in-flight work.
  3. @wire cannot be cancelled — if you need cancellation semantics, switch to imperative.
  4. Awaiting inside connectedCallback blocks rendering if synchronous. Prefer dispatching the async work and updating @track fields as results arrive.

Output Artifacts

ArtifactDescription
Async handler utilitytry/catch/finally + loading state template
AbortController wrapperCancellation helper for fetch/Apex
Retry-with-backoff helperShared exponential retry

Package files