Dev Encyclopedia
ArticlesTools
ArticlesTools

Get notified when new content drops

No spam. Just new articles, tools, and updates — straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

© 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /5 async/await Mistakes That Slow Your JavaScript Code
javascript8 min read

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.

By Dev EncyclopediaPublished May 30, 2026
On this page

On this page

  • Introduction
  • Sequential Awaits on Independent Tasks
  • await Inside forEach
  • Promise.all Without Failure Handling
  • The Silent Missing await
  • await on a map That Returns Promises
  • When Sequential Is the Right Choice
  • Quick Decision Guide
  • Frequently Asked Questions

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

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.

  • Promise.all kicks 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 await Task A first.
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),
]);

💡 Tip

Check every async function for sequential await calls on independent tasks. This single change can cut API response times by 2–3x on data-heavy pages.

2

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.

  • forEach calls each callback and moves on, ignoring any returned Promise.
  • Use for...of with await when tasks must run in order.
  • Use Promise.all + map when tasks can run in parallel.
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);
}

🚫 Danger

This produces no error or warning. The async callbacks run as fire-and-forget in the background. You have no way to know when they finish or if any of them fail.

3

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.all when you need every result and a partial failure genuinely means you can't continue.
  • Use Promise.allSettled when partial data is better than a full error — most dashboard UIs fall here.
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 : [];

ℹ Info

Promise.allSettled always resolves, never rejects. Each result object has status: 'fulfilled' or status: 'rejected' so you handle each outcome independently.

4

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 validatePost throws, the rejection is silently swallowed and savePost runs anyway.
  • Tests often miss this because they only check the happy path — the bug surfaces in production.
  • TypeScript with @typescript-eslint/no-floating-promises catches missing await calls statically.
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);
}

⚠ Warning

Run Node.js with --unhandled-rejections=strict during development. It crashes the process on silent promise rejections instead of ignoring them — a loud failure in dev beats a silent one in production.

5

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.

  • map returns a new array synchronously — it does not wait for async callbacks.
  • The outer await resolves the array synchronously (arrays are not Promises).
  • The fix is always await Promise.all(items.map(async item => fn(item))).
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 values

💡 Tip

A quick mental model: map with an async callback gives you an array of Promises. Promise.all turns an array of Promises into a single Promise that resolves with an array of values.

6

When 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.

ℹ Info

The question to ask before reaching for Promise.all: does this task need the previous result to start? If yes, await sequentially. If no, run in parallel.

7

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 await or for...of with await
  • 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.all doesn'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?
forEach calls each callback, receives the returned Promise, and immediately discards it. The callback is async so it returns a Promise, but forEach was designed before async/await existed and has no mechanism to wait for those Promises to settle. Execution continues synchronously to whatever follows the forEach call.
When should I use Promise.all vs Promise.allSettled?
Use Promise.all when 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. Use Promise.allSettled when 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-promises ESLint rule flags async function calls that aren't awaited. Running Node.js with --unhandled-rejections=strict turns 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.race resolves or rejects as soon as the first promise settles — useful for implementing timeouts (race a request against a timer). Promise.any resolves 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 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

nextjs

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.

May 30, 2026·7 min read
html css

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.

May 30, 2026·8 min read

On this page

  • Introduction
  • Sequential Awaits on Independent Tasks
  • await Inside forEach
  • Promise.all Without Failure Handling
  • The Silent Missing await
  • await on a map That Returns Promises
  • When Sequential Is the Right Choice
  • Quick Decision Guide
  • Frequently Asked Questions