async/await improves readability, but Promise APIs are often better for composition, fan-out concurrency, and shared in-flight work. Strong answers show the accidental-serialization pitfall and when raw Promise style stays clearer.
Use this JavaScript interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
When is Promise preferred over async/await in JavaScript?Frontend interview answer
This JavaScript interview question tests whether you can explain prefer Promise over async/await: concurrency and dedupe, connect it to production trade-offs, and handle common follow-up questions.
- prefer Promise over async/await: concurrency and dedupe explanation without falling back to memorized docs wording
- Promise and Async Await reasoning, edge cases, and production failure modes
- How you would answer the most likely JavaScript interview follow-up
The core idea
async/await is usually best for readable step-by-step logic. But Promises are often better when you need composition, fan-out/fan-in concurrency, or shareable in-flight results. The common production pitfall is hiding independent work behind sequential await calls and accidentally adding latency.
Strong interview answer: it is not Promise vs async/await as enemies; async/await is built on Promises. You choose the style that best expresses the control flow.
Situation | Prefer | Why Promise style is stronger |
|---|---|---|
Coordinate many async tasks |
| Combinators express concurrency and outcome policy directly. |
Share one in-flight request across callers | Store and reuse the same Promise | Avoid duplicate network calls and racey cache logic. |
Need fan-out/fan-in orchestration | Return Promise composition | Makes parallel start + single join point obvious. |
Library/middleware expects a Promise return | Return Promise chain directly | No extra wrapper code; keeps API surface simple. |
Example 1: side-by-side concurrency decision
This is the easiest place to lose performance: the slow version reads clearly, but it serializes independent requests.
async function loadDashboardSlow(userId) {
const user = await fetch(`/api/users/${userId}`).then((r) => r.json());
const orders = await fetch(`/api/orders/${userId}`).then((r) => r.json());
const recs = await fetch(`/api/recommendations/${userId}`).then((r) => r.json());
return { user, orders, recs };
}
function loadDashboardFast(userId) {
return Promise.all([
fetch(`/api/users/${userId}`).then((r) => r.json()),
fetch(`/api/orders/${userId}`).then((r) => r.json()),
fetch(`/api/recommendations/${userId}`).then((r) => r.json())
]).then(([user, orders, recs]) => ({ user, orders, recs }));
}
Example 2: in-flight dedupe cache (Promise is ideal)
A Promise can represent work already started. Multiple callers can await the same Promise safely, and a manual invalidation path keeps the cache story explicit.
const inFlight = new Map();
function getProduct(productId) {
if (inFlight.has(productId)) return inFlight.get(productId);
const p = fetch(`/api/products/${productId}`)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.finally(() => inFlight.delete(productId));
inFlight.set(productId, p);
return p;
}
function invalidateProduct(productId) {
inFlight.delete(productId);
}
Promise.all([getProduct('p1'), getProduct('p1')]).then(console.log);
Example 3: avoid unnecessary async wrappers
If a function simply forwards another Promise chain, returning the Promise directly can be clearer because the boundary stays composition-friendly.
function fetchSettings(userId) {
return fetch(`/api/settings/${userId}`).then((r) => r.json());
}
async function fetchSettings2(userId) {
const res = await fetch(`/api/settings/${userId}`);
return res.json();
}
Rule of thumb
Use async/await when the main thing you are expressing is a story-like sequence of business steps. Use raw Promise style when the main thing you are expressing is the relationship between async tasks: start together, dedupe, race, join, or hand one in-flight result to multiple callers.
Common pitfall | Impact | Fix |
|---|---|---|
Mixing styles randomly in one flow | Harder debugging and reviews | Choose one dominant style per function. |
Using sequential await for independent calls | Unnecessary latency | Start tasks together and join with |
Hiding shared in-flight work inside separate async functions | Duplicate requests and racey cache misses | Store and return the same Promise to all callers. |
Interview one-liner
Promise style is preferred when composing multiple concurrent operations, coordinating outcomes with combinators, or sharing in-flight asynchronous work across callers.
Practical scenario
A product page needs recommendations, inventory, and pricing from separate APIs. Running them sequentially with await adds unnecessary latency, while Promise combinators make fan-out/fan-in concurrency explicit. Meanwhile, product detail fetches may need one shared in-flight Promise so two widgets do not trigger duplicate requests.
Common pitfalls
- Using
awaitinside loops without checking if calls are independent. - Wrapping a Promise-returning utility in
asynceven though the wrapper adds no clarity. - Ignoring invalidation for shared in-flight caches.
Test both fast-success and partial-failure cases. Add latency tests to confirm the refactor actually improves total response time, and test duplicate callers against one shared Promise path.
Use async/await when reading the logic like a story matters most. Use raw Promise APIs when coordinating many moving async pieces is the real challenge.
Use this as one explanation rep, then continue with the JavaScript interview questions cluster or a guided prep path.