5 async/await Mistakes That Slow Your JavaScript Code
Sequential awaits, await in forEach, missing Promise.all — these 5 async/await mistakes silently slow your JavaScript. Here's how to spot and fix each one.
On this page
Introduction
If you've ever wondered why your dashboard takes 3 seconds to load when your API calls each only take 1 second — this post is for you.
async/await makes asynchronous code look clean. It also makes it very easy to accidentally run things in sequence that should run in parallel. Here are five mistakes that show up in real production code, with before/after examples and exactly how to fix each one.
Step-by-Step Guide
Sequential Awaits When Tasks Are Independent
The most expensive performance mistake in async JavaScript. When tasks don't depend on each other, awaiting them one-by-one means 3 one-second calls take 3 seconds instead of 1.
Promise.allkicks off all three requests simultaneously and waits for all of them to finish.- The total time equals the duration of the slowest request, not the sum of all requests.
- The rule: if Task B doesn't need Task A's result, don't
awaitTask A first.
// ❌ Sequential — 3 requests × 1 second = 3 seconds total
const user = await fetchUser(userId);
const posts = await fetchPosts(userId);
const stats = await fetchAnalytics(userId);
// ✅ Parallel with Promise.all — ~1 second (slowest request wins)
const [user, posts, stats] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchAnalytics(userId),
]);await Inside forEach
forEach was not designed to understand Promises. When you mark its callback async, it starts each promise but doesn't wait — execution continues immediately.
forEachcalls each callback and moves on, ignoring any returned Promise.- Use
for...ofwithawaitwhen tasks must run in order. - Use
Promise.all+mapwhen tasks can run in parallel.
// ❌ await inside forEach — fire-and-forget
orderIds.forEach(async (id) => {
await processOrder(id); // not waited for
});
console.log('Done!'); // runs immediately — nothing has processed yet
// ✅ Parallel: Promise.all + map
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('Done!'); // waits for all orders
// ✅ Sequential: for...of with await
for (const id of orderIds) {
await processOrder(id);
}Promise.all Without Handling Partial Failures
Promise.all is fast but unforgiving — if one promise rejects, the entire call rejects and you lose results from every promise that succeeded.
- Use
Promise.allwhen you need every result and a partial failure genuinely means you can't continue. - Use
Promise.allSettledwhen partial data is better than a full error — most dashboard UIs fall here.
// ❌ One failure kills all three results
const [user, posts, stats] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchAnalytics(userId), // throws 500 → you get nothing
]);
// ✅ Promise.allSettled — partial results on failure
const results = await Promise.allSettled([
fetchUser(userId),
fetchPosts(userId),
fetchAnalytics(userId),
]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];The Silent Missing await
The most dangerous mistake: forgetting await on an async function produces no error. The operation runs fire-and-forget and execution continues immediately.
- If
validatePostthrows, the rejection is silently swallowed andsavePostruns anyway. - Tests often miss this because they only check the happy path — the bug surfaces in production.
- TypeScript with
@typescript-eslint/no-floating-promisescatches missingawaitcalls statically.
// ❌ Missing await — validation runs but isn't waited for
async function createPost(data) {
validatePost(data); // fires and is forgotten
return await savePost(data); // runs before validation finishes
}
// ✅ With await — validation must complete before saving
async function createPost(data) {
await validatePost(data);
return await savePost(data);
}Awaiting a map That Returns Promises
When you use async inside a map callback, map returns an array of Promises, not resolved values. Awaiting the array itself resolves immediately — it's not a Promise.
mapreturns a new array synchronously — it does not wait for async callbacks.- The outer
awaitresolves the array synchronously (arrays are not Promises). - The fix is always
await Promise.all(items.map(async item => fn(item))).
// ❌ await on the array — resolves immediately with [Promise, Promise, Promise]
const results = await items.map(async (item) => fetchData(item));
// results is [Promise {}, Promise {}, Promise {}], not actual values
// ✅ Wrap with Promise.all — resolves all promises in the array
const results = await Promise.all(
items.map(async (item) => fetchData(item))
);
// results is now the actual fetched valuesWhen Sequential Is the Right Choice
Not every situation calls for parallel execution. Sequential await is the correct choice in specific scenarios:
- Task B depends on Task A's result — fetch a user, then fetch their orders using the user ID.
- You are writing to a database where concurrent writes on the same record create race conditions.
- You are processing a queue where order matters and each item must complete before the next begins.
Quick Decision Guide
Pick the right async pattern by asking whether each task depends on the previous result:
- Independent tasks, need all results:
await Promise.all([task1(), task2(), task3()]) - Independent tasks, partial failures acceptable:
await Promise.allSettled([task1(), task2()]) - Sequence where each result feeds the next: sequential
awaitorfor...ofwithawait - Array of items processed in parallel:
await Promise.all(items.map(async item => fn(item))) - Array of items processed one at a time:
for (const item of items) { await fn(item); }
Frequently Asked Questions
- Does Promise.all actually run tasks in parallel?
- For I/O-bound work like network requests and database queries — yes. The async operations are initiated simultaneously and run concurrently at the network/OS level. JavaScript is single-threaded, so
Promise.alldoesn't help with CPU-bound computation, but most async bottlenecks in web applications are I/O, not CPU. - Why doesn't await inside forEach work?
forEachcalls each callback, receives the returned Promise, and immediately discards it. The callback is async so it returns a Promise, butforEachwas designed before async/await existed and has no mechanism to wait for those Promises to settle. Execution continues synchronously to whatever follows theforEachcall.- When should I use Promise.all vs Promise.allSettled?
- Use
Promise.allwhen every result is required and a single failure means you genuinely can't continue — for example, fetching critical data where missing any piece breaks the entire UI. UsePromise.allSettledwhen partial success is acceptable and you want to show as much data as you have — dashboard UIs that can display user and posts even if analytics is down are a better user experience than a full error page. - How do I catch missing await bugs?
- Three tools help: TypeScript with the
@typescript-eslint/no-floating-promisesESLint rule flags async function calls that aren't awaited. Running Node.js with--unhandled-rejections=strictturns silent rejections into loud crashes during development. Writing tests that intentionally trigger the async path with failures will surface missing awaits that the happy path misses. - When would I use Promise.race or Promise.any?
Promise.raceresolves or rejects as soon as the first promise settles — useful for implementing timeouts (race a request against a timer).Promise.anyresolves as soon as the first promise fulfills — useful for redundant requests where you want the fastest successful result and don't care about failures. Both are advanced patterns for specific use cases, not general alternatives toPromise.all.
Most async performance bugs aren't complicated — they're await calls that should be Promise.all, and forEach loops that should be for...of.
Make a habit of asking one question before every await: does this task need the previous result? That question catches most of the mistakes in this post.
Related Articles
How to Use Environment Variables in Next.js (Without Leaking Them to the Browser)
Learn how to use .env files in Next.js correctly. Understand NEXT_PUBLIC_, avoid common mistakes, and set variables in Vercel and Cloudflare.
8 CSS :has() Patterns You'll Actually Use (2026)
CSS :has() is production-ready in every browser. Here are 8 real-world patterns — form states, sibling dimming, modal scroll-lock, and more.