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
AbortControllerto 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
finallyresetting loading state. - For multiple calls, use
Promise.allwhen order doesn't matter. - Add
AbortControlleror 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
- try/catch wraps every imperative call
-
finallyresets loading state -
Promise.allused 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
- Imperative Apex errors have shape
{body: {message}}— not.messagedirectly. Alwayserr.body?.message. - LWC does NOT expose a
componentWillUnmountlifecycle hook. UsedisconnectedCallbackto abort in-flight work. @wirecannot be cancelled — if you need cancellation semantics, switch to imperative.- Awaiting inside
connectedCallbackblocks rendering if synchronous. Prefer dispatching the async work and updating@trackfields 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 tradeoffslwc/lwc-show-toast-patterns— error feedbackapex/apex-http-callout-mocking— server-side mock tests