← All Articles 🐌 Performance Anti-Patterns High By Nishant Kumar Verma Updated

await Inside Array.map(): The Silent Performance Killer in AI Code

AI tools put await inside .map() creating sequential execution instead of parallel. Learn the fix with Promise.all().

PERF-001 PERF-005

Quick Answer

Putting await inside an async callback passed to Array.map() does not make .map() wait. It starts every callback at once and hands back an array of Promises, so if nothing collects that array, your code moves on before the work is done. The fix is to wrap the map in Promise.all(), which waits for all of the operations to finish. The truly sequential version is await inside a for...of loop; when the calls are independent, switching it to Promise.all() cuts the wait from the sum of the calls to the slowest one.

How await Inside .map() Actually Executes

The mistake is a subtle misunderstanding of how JavaScript's async model works. When you write array.map(async item => await doSomething(item)), each callback does start an async operation - but the callback returns a Promise immediately and .map() moves to the next item. Without Promise.all(), nothing is actually waiting on those Promises. If you add await inside the callback, you are awaiting inside that individual callback's execution, which means each callback waits for its own operation before finishing - but .map() does not wait for any of them.

The result depends on exactly how you write it. There are two common patterns, both wrong in different ways:

Pattern 1 - Silent bug, unfinished results: You await array.map() directly, or never await it at all. Awaiting a plain array of Promises, not a single Promise, resolves immediately with the array of still-pending Promises. You process unresolved data, and a rejection goes unhandled. This is the pattern PERF-001 flags: an async callback in .map() whose array is never passed to Promise.all(), allSettled(), race() or any().

Pattern 2 - Sequential execution: You use await inside a for...of loop, or chain several awaits inside one callback, so each call starts only after the previous one ends.

The Kind of Code AI Generates

Here is a typical version of code that fetches user data for a list of IDs:

// ❌ BAD - The two fetches for each user run one after the other
export async function getUsersSequential(userIds: string[]) {
  const users = await Promise.all(
    userIds.map(async (id) => {
      // The users run in parallel, but the profile fetch waits for the user fetch
      const user = await fetch(`/api/users/${id}`).then(r => r.json());
      const profile = await fetch(`/api/profiles/${id}`).then(r => r.json());
      return { ...user, ...profile };
    })
  );
  return users;
}
// For 10 users: two round trips back to back (user, then profile)
// Typical time: 200ms × 2 = ~400ms

This pattern looks like it should be parallel - it uses Promise.all() at the outer level. But the inner awaits are sequential: for each user, it fetches the user record, waits for it to complete, then fetches the profile. The outer Promise.all() does run the per-user processing concurrently, but within each user's processing, the two fetches are sequential.

The Fix: True Parallelism with Promise.all()

// ✅ GOOD - Full parallelism
export async function getUsersParallel(userIds: string[]) {
  const users = await Promise.all(
    userIds.map(async (id) => {
      // Both fetches fire simultaneously for each user
      const [user, profile] = await Promise.all([
        fetch(`/api/users/${id}`).then(r => r.json()),
        fetch(`/api/profiles/${id}`).then(r => r.json()),
      ]);
      return { ...user, ...profile };
    })
  );
  return users;
}
// For 10 users: all 20 fetches fire in parallel
// Typical time: ~200ms (one round-trip latency, not two in a row)

The outer Promise.all() runs each user's processing in parallel. The inner Promise.all() runs both fetches for each user in parallel. The total wall-clock time equals the time of the slowest single operation, not the sum of all operations.

For 10 users with two 200ms fetches each, the first version takes approximately 400ms and the fully parallel version approximately 200ms: twice as fast with a two-line change. A truly sequential version, with both awaits inside a for...of loop, makes the 20 calls one at a time and takes approximately 4,000ms, 20 times slower than the parallel one.

When Sequential Execution Is Intentional

Not every sequential async loop is a bug. There are legitimate cases where you need operations to run in order:

Pattern Use When Code
Sequential (for...of + await) Each operation depends on the previous result; rate-limited APIs; ordered inserts for (const item of items) { await process(item); }
Parallel (Promise.all) Independent operations; fetching multiple records; sending multiple notifications await Promise.all(items.map(async item => process(item)))
Controlled concurrency (p-limit) Parallel but rate-limited; database connection pool limits; external API quotas const limit = pLimit(5); await Promise.all(items.map(item => limit(() => process(item))))
Streaming (for await...of) Processing a stream; paginated API responses; large datasets for await (const chunk of stream) { process(chunk); }

The key question is: does item B need item A's result before it can start? If yes, sequential is correct. If no, parallelize. Sequential code shows up so often because it is more readable and easier to reason about step-by-step - but that reasoning prioritizes code clarity over runtime performance.

The PERF-005 Variant: Unnecessary Sequential Await Chains

PERF-005 covers a related pattern: sequential await chains outside loops where operations are independent. AI-generated Next.js API routes frequently produce this:

// ❌ BAD - Sequential independent operations (PERF-005)
export async function GET(req: Request) {
  const user = await getUser(req);        // 50ms
  const settings = await getSettings();   // 40ms
  const features = await getFeatures();   // 30ms
  // Total: ~120ms - each waits for the previous

  return Response.json({ user, settings, features });
}

// ✅ GOOD - Parallel independent operations
export async function GET(req: Request) {
  const [user, settings, features] = await Promise.all([
    getUser(req),      // all three fire simultaneously
    getSettings(),
    getFeatures(),
  ]);
  // Total: ~50ms - time of the slowest operation

  return Response.json({ user, settings, features });
}

Sequential awaits on independent operations are easy to write and easy to miss in review. The pattern is common even in experienced teams' code, and AI tools that generate straightforward sequential code add more of it.

How to Find This Pattern in Your Codebase

Manual code review works for small files but does not scale. Here are automated approaches:

  1. ESLint rule: The no-await-in-loop rule catches awaits inside for loops. It does not cover .map() callbacks. For those, typescript-eslint's no-floating-promises flags an array of Promises that nothing handles, and await-thenable flags await on a value that is not a Promise, such as the array .map() returns. Both need type information.
  2. Search your codebase: Look for the pattern async.*=>.*await inside .map( calls. Any match where the operations are independent is a candidate for Promise.all().
  3. Automated scanning: Tools like VibeDoctor's Vibe Check automatically scan your codebase for async .map() callbacks whose Promises are never collected (PERF-001) and runs of independent sequential awaits (PERF-005), and flag specific file paths and line numbers. Free to sign up.
  4. Timing and tracing: Log the start and end time of each call, or trace the request with your APM. Sequential awaits show up as calls that start only after the previous one ends, a staircase in the trace waterfall.

Running independent awaits in parallel cuts API response time: the route waits for the slowest call instead of the sum of all of them.

FAQ

Does Promise.all() fail if one operation throws?

Yes. If any Promise in the array rejects, Promise.all() rejects immediately with that error, and the other Promises continue running but their results are discarded. If you need all operations to complete regardless of individual failures, use Promise.allSettled() instead. It always resolves with an array of result objects, each with a status of 'fulfilled' or 'rejected'.

Can running too many Promises in parallel cause problems?

Yes. Firing hundreds of database queries or HTTP requests simultaneously can overwhelm connection pools or trigger rate limits. For large arrays, use a concurrency limiter like the p-limit package. A limit of 5-10 concurrent operations is a reasonable default for most APIs and database pools.

Is for...of with await always bad?

No. For sequential dependencies - like processing a payment pipeline where each step depends on the previous - for...of with await is exactly right. The issue is using it for independent operations where parallelism is safe. PERF-001 does not look at for...of loops at all; it flags async .map() callbacks whose Promises are never collected with Promise.all(), allSettled(), race() or any().

Does this apply to React Server Components?

Yes. Server Components can fetch data in parallel with Promise.all() - the same pattern applies. The Next.js data fetching docs show that awaits placed one after another in a component run sequentially, and recommend starting the requests first and awaiting them together with Promise.all().

What is the difference between PERF-001 and PERF-005?

PERF-001 targets async callbacks inside .map() whose array of Promises is never passed to Promise.all() or a sibling. PERF-005 targets sequential await chains at the function level - three or more const x = await lines in a row where none uses an earlier result, so they could be batched into a single Promise.all(). The first can leave work unfinished; the second wastes wall-clock time.

Diagnose your codebase - free

VibeDoctor checks for PERF-001, PERF-005 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →