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
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.
- 1
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.
javascript// ❌ 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), ]);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.
- 2
await Inside forEach
forEachwas not designed to understand Promises. When you mark its callbackasync, it starts each promise but doesn't wait: execution continues immediately.javascript// ❌ 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); }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.
- 3
Promise.all Without Handling Partial Failures
Promise.allis fast but unforgiving: if one promise rejects, the entire call rejects and you lose results from every promise that succeeded.javascript// ❌ 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 : [];- 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.
- Use
- 4
The Silent Missing await
The most dangerous mistake: forgetting
awaiton an async function produces no error. The operation runs fire-and-forget and execution continues immediately.javascript// ❌ 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); }- 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.
Enable the ESLint rule that catches this automatically:
javascript — eslint.config.mjsimport tseslint from 'typescript-eslint'; export default tseslint.config({ rules: { '@typescript-eslint/no-floating-promises': 'error', }, }); - If
- 5
Awaiting a map That Returns Promises
When you use
asyncinside amapcallback,mapreturns an array of Promises, not resolved values. Awaiting the array itself resolves immediately, since it's not a Promise. Related to Mistake 2 (forEach) but with a different cause.javascript// ❌ 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 valuesmapreturns 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))).
- 6
When Sequential Is the Right Choice
Not every situation calls for parallel execution. Sequential
awaitis 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.
javascript// ✅ Sequential (fetchOrders needs userId from the user object) const user = await fetchUser(userId); const orders = await fetchOrders(user.id); // depends on user resultjavascript// ✅ Sequential queue (order matters, concurrent writes risk race conditions) for (const item of queue) { await processItem(item); }
Quick Decision Guide
Pick the right async pattern based on whether tasks depend on each other's results:
| Situation | Pattern to use |
|---|---|
| Independent tasks, need all results | await Promise.all([a(), b(), c()]) |
| Independent tasks, partial failure OK | await Promise.allSettled([a(), b()]) |
| Each result feeds the next task | Sequential await or for...of with await |
| Array of items processed in parallel | await Promise.all(items.map(async i => fn(i))) |
| Array processed one at a time | for (const item of items) { await fn(item); } |
Frequently Asked Questions
Does Promise.all actually run tasks in parallel?
- I/O-bound work (network requests, database queries): yes. Operations are initiated simultaneously and run concurrently at the network/OS level.
- CPU-bound computation: no. JavaScript is single-threaded, so
Promise.allcan't help here. - Most async bottlenecks in web apps are I/O, not CPU, so
Promise.allgives real speed gains in practice.
Why doesn't await inside forEach work?
forEachcalls each async callback and immediately discards the returned Promise.- It was designed before async/await existed and has no mechanism to wait for Promises.
- Execution continues synchronously after the
forEachcall, before any of the async work is done.
// forEach (fire-and-forget, broken)
items.forEach(async (item) => await fn(item));
// for...of (sequential, correct)
for (const item of items) { await fn(item); }
// Promise.all + map (parallel, correct)
await Promise.all(items.map(item => fn(item)));When should I use Promise.all vs Promise.allSettled?
| Promise.all | Promise.allSettled | |
|---|---|---|
| Rejects if any fails? | Yes (entire call rejects) | No (always resolves) |
| Use when | Every result is required | Partial success is acceptable |
| Best for | Critical data where missing = broken UI | Dashboards that show as much as they can |
How do I catch missing await bugs?
- `@typescript-eslint/no-floating-promises`: ESLint rule that flags async function calls without
await. See Step 4 for the config. - `--unhandled-rejections=strict`: Node.js flag that crashes the process on silent rejections during development.
- Tests: write tests that intentionally trigger the async path with failures to surface missing awaits.
When would I use Promise.race or Promise.any?
- `Promise.race`: resolves or rejects as soon as the first promise settles. Use for timeouts: race a request against a timer.
- `Promise.any`: resolves as soon as the first promise fulfills. Use for redundant requests where you want the fastest success.
- Both are advanced patterns for specific use cases, not general alternatives to
Promise.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.
Drizzle ORM Migrations: A Practical drizzle-kit Guide
Learn the full Drizzle ORM migration workflow: push vs migrate, drizzle-kit setup, Turso/libSQL config, team conflicts, and production best practices.