lwc-async-patterns

star 33

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 wire service basics (use lwc-wire-refresh-patterns). NOT for Lightning Data Service.

PranavNagrecha By PranavNagrecha schedule Updated 6/16/2026

name: lwc-async-patterns description: "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 wire service basics (use lwc-wire-refresh-patterns). NOT for Lightning Data Service." category: lwc salesforce-version: "Spring '25+" well-architected-pillars: - Performance - User Experience tags: - lwc - async - promise - imperative-apex - abortcontroller triggers: - "lwc imperative apex async await loading state" - "lwc promise.all parallel apex call performance" - "lwc abortcontroller cancel inflight fetch on navigate" - "lwc unhandled promise rejection swallowed error" - "lwc wire + imperative call race condition" - "async error lwc toast user feedback" inputs: - Data needs (imperative vs wire) - Parallel vs serial requirements - Cancellation semantics outputs: - Async handler with try/catch + finally - Loading-state management - AbortController usage (where applicable) dependencies: [] version: 1.1.0 author: Pranav Nagrecha updated: 2026-05-19

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

  • Decide imperative vs wire. Wire is reactive and caches; imperative runs on demand and surfaces errors/loading cleanly.
  • Identify parallel opportunities. Multiple independent calls → Promise.all.
  • Plan cancellation. Long-running fetches need AbortController to avoid setState-on-unmounted.

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

Situation Approach
Reactive data that updates with context @wire
On-demand fetch (button click) imperative + async/await
Multiple independent calls Promise.all
Partial-success acceptable Promise.allSettled
User can navigate away mid-fetch AbortController
User rapidly retypes a query debounce + cancel prior

Recommended Workflow

  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

  • try/catch wraps every imperative call
  • finally resets loading state
  • Promise.all used where calls are independent
  • Stale-response guard (AbortController or request id) present for filter/search UIs
  • Unmount path cancels in-flight work
  • Error branch displays meaningful message (not raw Error stringify)
  • No unhandled promise rejections (don't swallow errors silently)

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

Artifact Description
Async handler utility try/catch/finally + loading state template
AbortController wrapper Cancellation helper for fetch/Apex
Retry-with-backoff helper Shared exponential retry

Related Skills

  • lwc/lwc-wire-refresh-patterns — wire vs imperative tradeoffs
  • lwc/lwc-show-toast-patterns — error feedback
  • apex/apex-http-callout-mocking — server-side mock tests
Install via CLI
npx skills add https://github.com/PranavNagrecha/AwesomeSalesforceSkills --skill lwc-async-patterns
Repository Details
star Stars 33
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator
PranavNagrecha
PranavNagrecha Explore all skills →