How do you handle asynchronous code using async/await and Promises
Default to async/await for readability. Use Promise.all for parallel independent work, Promise.allSettled when you want every result regardless of failure, Promise.race / any for first-to-finish. Always wrap awaits in try/catch (or .catch on the call site). Pass an AbortSignal for cancellation. Never await sequentially when work is independent.
Async/await for sequential reading
async function load(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return { user, posts };
}Reads like sync; debugger stops at each line. Use this as the default.
Parallel independent work
const [user, prefs] = await Promise.all([fetchUser(id), fetchPrefs(id)]);Two unrelated calls — fire both at once, await both. Don't write await fetchUser; await fetchPrefs — that's sequential and wastes a round trip.
When a parallel call may fail
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
for (const r of results) {
if (r.status === "fulfilled") use(r.value);
else log(r.reason);
}Promise.all rejects on the first failure; allSettled always resolves with per-promise outcomes.
Race / first-to-finish
const winner = await Promise.race([fetch(url), timeout(5000)]);Promise.any resolves on the first fulfilled (rejects only if all reject) — handy for "try multiple mirrors, take the first to succeed."
Error handling
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
if (err.name === "AbortError") return null;
log(err);
throw err; // or wrap in domain error
}
}Wrap awaits in try/catch — or let the rejection propagate and catch at a higher boundary (component error boundary, request handler).
Cancellation
const ac = new AbortController();
fetch(url, { signal: ac.signal }).then(...).catch((e) => {
if (e.name === "AbortError") return;
throw e;
});
ac.abort();For React: pass signal in useEffect; abort on cleanup.
Sequential when needed
If step B depends on step A's value, sequential is correct:
const user = await fetchUser();
const posts = await fetchPosts(user.id); // depends on userIf they're independent, don't sequence them.
Streaming results
For early progress, don't await all — fire and render as each resolves:
fetchA().then(setA);
fetchB().then(setB);Or with React Suspense:
<Suspense fallback={<Skeleton />}>
<UserData id={id} />
</Suspense>Common mistakes
- Sequential awaits for independent work.
- Forgetting to await a Promise (returns Promise instead of value).
map(async ...)and notPromise.all-ing the result.- Throwing inside async expecting sync caller to catch (rejection is async).
- Unhandled rejections crashing Node.
Interview framing
"Async/await for readability by default. Promise.all for independent work that should run in parallel — biggest perf bug I see is sequential awaits. Promise.allSettled when failures are acceptable. Promise.race / Promise.any for timeouts and first-successful patterns. Wrap awaits in try/catch or let them propagate to an error boundary. Pass AbortSignal for cancellation — critical in React effects and aborting stale requests. The two patterns that catch people: map(async ...) without Promise.all, and forgetting to await before logging the result."
Follow-up questions
- •Compare Promise.all and Promise.allSettled.
- •When have you used Promise.race?
- •How does AbortController integrate with fetch?
Common mistakes
- •Sequential awaits for independent work.
- •Missing await.
- •map(async) without Promise.all.
- •No try/catch around awaits with side effects.
Performance considerations
- •Parallelizing independent work saves N-1 RTTs. Cancellation prevents wasted compute on stale data.
Edge cases
- •Promise.all([]) resolves to [] immediately.
- •Returning a Promise from an async function — it gets flattened.
- •Top-level await behavior in modules.
Real-world examples
- •React Query, server-side data loaders, batch ETL, parallel API aggregation.