| 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-19T00:00:00.000Z" |
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
- Classify each call as wire or imperative.
- For imperative, wrap in try/catch with
finally resetting loading state.
- For multiple calls, use
Promise.all when order doesn't matter.
- Add
AbortController or request-id guards to prevent stale updates.
- Surface errors via
ShowToastEvent (sticky for errors) OR inline banner.
- Test with slow-network throttling in browser devtools.
- Ensure unmount path clears pending timers / aborts controllers.
Review Checklist
Salesforce-Specific Gotchas
- Imperative Apex errors have shape
{body: {message}} — not .message directly. Always err.body?.message.
- LWC does NOT expose a
componentWillUnmount lifecycle hook. Use disconnectedCallback to abort in-flight work.
@wire cannot be cancelled — if you need cancellation semantics, switch to imperative.
- 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