Dev Encyclopedia
ArticlesToolsContactAbout

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
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  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.

Zeeshan Tofiq
Zeeshan Tofiq
May 30, 2026
On this page

On this page

  • 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

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

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

    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);
    }
    • 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.

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

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

    ℹ Info

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

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

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

    Enable the ESLint rule that catches this automatically:

    javascript — eslint.config.mjs
    import tseslint from 'typescript-eslint';
    
    export default tseslint.config({
      rules: {
        '@typescript-eslint/no-floating-promises': 'error',
      },
    });

    ⚠ Warning

    Also 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. 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, 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 values
    • 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))).

    💡 Tip

    Mental model: map with an async callback gives you an array of Promises. Promise.all turns that array into a single Promise resolving with the array of values.

  6. 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.
    javascript
    // ✅ Sequential (fetchOrders needs userId from the user object)
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id); // depends on user result
    javascript
    // ✅ Sequential queue (order matters, concurrent writes risk race conditions)
    for (const item of queue) {
      await processItem(item);
    }

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

Quick Decision Guide

Pick the right async pattern based on whether tasks depend on each other's results:

SituationPattern to use
Independent tasks, need all resultsawait Promise.all([a(), b(), c()])
Independent tasks, partial failure OKawait Promise.allSettled([a(), b()])
Each result feeds the next taskSequential await or for...of with await
Array of items processed in parallelawait Promise.all(items.map(async i => fn(i)))
Array processed one at a timefor (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.all can't help here.
  • Most async bottlenecks in web apps are I/O, not CPU, so Promise.all gives real speed gains in practice.
Why doesn't await inside forEach work?
  • forEach calls 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 forEach call, before any of the async work is done.
javascript
// 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.allPromise.allSettled
Rejects if any fails?Yes (entire call rejects)No (always resolves)
Use whenEvery result is requiredPartial success is acceptable
Best forCritical data where missing = broken UIDashboards 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.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.

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·12 min read
databases

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.

May 30, 2026·9 min read

On this page

  • 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
Advertisement