Mid-level 9 min · March 05, 2026
async and await in JavaScript

async forEach Fires 1000 Calls, Awaits Zero

forEach ignores async callbacks—1000 payments fired at once, 700 failed silently with zero errors logged.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • async/await is syntactic sugar over Promises—async functions return a Promise, await pauses function execution without blocking the thread
  • Core components: async keyword (marks function as asynchronous), await (waits for Promise resolution), Promise.all (parallel execution), try/catch (error handling)
  • Performance: 3 sequential awaits each taking 300ms = 900ms; Promise.all with same tasks = 300ms (3x faster for independent operations)
  • Production trap: Using array.forEach(async item => { await process(item) }) — forEach ignores returned Promises, runs all in parallel and never waits
  • Biggest mistake: Forgetting that fetch() only rejects on network errors, not HTTP 404/500 — must check response.ok or risk silent failures
✦ Definition~90s read
What is async and await in JavaScript?

async/await is syntactic sugar over JavaScript's Promise system, introduced in ES2017. Under the hood, async functions return a Promise, and await pauses execution of that function — not the entire thread — until the awaited Promise settles. This is possible because JavaScript's event loop handles the suspension and resumption via microtask queues.

Imagine you order a pizza and sit by the phone waiting for it — you can't do anything else until it arrives.

The critical thing to understand: await only blocks within its own async function scope. It does not block the call stack or other async functions running concurrently. This is why array.forEach(async callback) is a footgun — the forEach method itself is synchronous and does not await the Promises returned by its callbacks.

It fires all callbacks immediately, and any await inside those callbacks only pauses that individual callback, not the loop. The result: 1000 network requests fire in parallel with zero coordination, overwhelming APIs and losing error context. For sequential execution, use a for...of loop with await inside.

For controlled concurrency, use Promise.all with batching or libraries like p-limit. The performance trap is real: parallel is faster but dangerous without rate limiting; sequential is safe but slow. Real-world resilient API services use a hybrid — sequential for dependent calls, parallel with concurrency limits for independent ones, and always wrap every await in try/catch with proper error recovery.

Plain-English First

Imagine you order a pizza and sit by the phone waiting for it — you can't do anything else until it arrives. That's old-school synchronous code. async and await are like placing the order, going to watch TV, and trusting the doorbell will ring when the pizza's ready. Your program keeps doing useful work while it waits for slow things like network requests or file reads, and 'await' is just the doorbell — it tells JavaScript: 'pause here until this one thing is done, then carry on.'

Every real app talks to something slow — a database, an API, a file system. If JavaScript stopped and stared at the wall waiting for each of those operations to finish, your UI would freeze, your server would stall, and your users would leave. Asynchronous programming is the reason JavaScript can handle thousands of requests without breaking a sweat, and async/await is the cleanest way to write that kind of code today.

Before async/await landed in ES2017, developers handled async operations with callbacks, then Promises. Both work, but callbacks turn into a pyramid of doom and raw Promise chains get noisy fast. async and await don't replace Promises — they're syntactic sugar that sits on top of them, letting you write async code that reads like normal top-to-bottom synchronous code, which is how your brain naturally thinks.

By the end you'll understand exactly why async/await exists, how it maps to the Promise model underneath, how to handle errors properly, and how to avoid the performance trap that catches most intermediate developers — running async operations in sequence when they could run in parallel. You'll write better, faster, more readable async code starting today.

What async and await Actually Do Under the Hood

The async keyword does one thing: it makes a function always return a Promise. That's it. If you return a plain value like 42, JavaScript quietly wraps it in Promise.resolve(42). This matters because it means every async function plugs seamlessly into the existing Promise ecosystem — you can chain .then() on an async function's return value if you ever need to.

The await keyword can only live inside an async function. It pauses that function's execution, hands control back to the event loop so other code can run, then resumes the function once the Promise it's waiting on settles. Critically, await does NOT block the thread — it blocks only that specific function's execution while the rest of your program continues running.

Think of async functions as generators with built-in Promise plumbing. The JavaScript engine essentially transforms your await expressions into .then() callbacks at compile time. This is why understanding Promises first makes async/await click immediately — you're not learning something new, you're learning a cleaner way to write what you already know.

This mental model prevents a lot of confusion. When something goes wrong with async/await, the debugging answer almost always lives in understanding the underlying Promise behavior.

io/thecodeforge/js/asyncUnderTheHood.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ─── Demonstrating that async functions always return a Promise ───

async function getWelcomeMessage() {
  // We return a plain string, but the async keyword
  // automatically wraps this in Promise.resolve('Hello!')
  return 'Hello from an async function!';
}

// Proof: call it and check what we get back
const result = getWelcomeMessage();
console.log(result instanceof Promise); // true — it's always a Promise

// Use .then() on it — totally valid because it IS a Promise
result.then((message) => console.log(message));

// ─── Demonstrating what await actually does ───

function simulateNetworkDelay(ms) {
  // Returns a Promise that resolves after `ms` milliseconds
  // This mimics a real API call or database query
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchUserProfile() {
  console.log('1. fetchUserProfile started');

  // await pauses THIS function but NOT the whole program.
  // The event loop is free to run other code during this wait.
  await simulateNetworkDelay(1000);

  console.log('3. fetchUserProfile resumed after 1 second');
  return { id: 42, name: 'Sarah Connor', role: 'engineer' };
}

console.log('0. Before calling fetchUserProfile');
fetchUserProfile().then((user) => console.log('4. Got user:', user.name));
console.log('2. After calling fetchUserProfile — proof the thread is NOT blocked');
Key Mental Model:
async/await is compiled Promise chains. Every await X is equivalent to .then(() => X). Knowing this means you can debug any async/await problem by mentally translating it back into Promise syntax.
Production Insight
async/await does NOT make code run faster. It frees the thread to do other work during I/O waits, improving concurrency.
The event loop runs other tasks while async function is suspended at await. This is how Node.js achieves high concurrency with one thread.
Rule: Use async/await for I/O-bound operations (network, file, database). For CPU-bound work, use Worker Threads or offload to separate process.
Key Takeaway
async functions always return a Promise — even if you return a plain value like a string or number, it gets wrapped in Promise.resolve() automatically.
await only blocks the current async function's execution, not the JavaScript thread — the event loop continues running other code while it waits.
Rule: Every await is a .then() under the hood. Understanding Promises helps debug async/await issues.
to await or not to await
IfYou need the result of the Promise before continuing
UseUse await. The function will pause at that line until the Promise resolves, then continue with the value.
IfYou don't need the result (fire-and-forget), but must handle errors
UseCall the async function without await, but attach .catch(): doWork().catch(console.error). Use void doWork() to indicate intentional no-await.
IfYou need to run multiple independent Promises in parallel
UseUse Promise.all([async1(), async2()]) with await. This runs all concurrently, not sequentially. Total time = max of durations.
IfYou need to run Promises in sequence (each depends on previous)
UseUse separate await statements: const a = await step1(); const b = await step2(a);. Each waits for previous to complete.
IfYou need to start a Promise now but await it later
UseStore the Promise in a variable: const promise = asyncOp(); ... later await promise. The operation starts immediately, awaiting just waits for completion.
async forEach: 1000 Calls, Zero Awaits THECODEFORGE.IO async forEach: 1000 Calls, Zero Awaits How async/await behaves in forEach vs proper patterns async forEach Callback Fires all 1000 calls without awaiting Try/Catch Wrapping Wrap each call to catch errors Sequential Execution Use for...of to await one by one Parallel Execution Use Promise.all for concurrent calls Concurrency Control Batch with limit to avoid overload ⚠ forEach ignores async — fires all promises instantly Use for...of or Promise.all with batching THECODEFORGE.IO
thecodeforge.io
async forEach: 1000 Calls, Zero Awaits
Async Await Javascript

Error Handling in async/await — Do This, Not That

This is where most developers take a wrong turn. A rejected Promise inside an async function that has no error handling will give you an UnhandledPromiseRejection warning and silently swallow the error in some environments. You need a strategy — and the right strategy depends on your context.

The most readable pattern is try/catch, which lets you handle errors exactly the way you'd handle synchronous errors. This is one of the biggest wins of async/await — unified error handling between sync and async code in a single try/catch block.

But try/catch has a trap: if you need to differentiate between different kinds of failures in one block, it gets messy. In those cases, a small helper pattern — sometimes called a 'safe await' or 'to' pattern — lets you handle errors inline without a try/catch tower.

The golden rule: never use async/await without an error handling strategy. An async function that can fail and has no .catch() or try/catch is a time bomb. In production Node.js apps, unhandled Promise rejections can crash the process entirely since Node 15.

io/thecodeforge/js/asyncErrorHandling.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// ─── Pattern 1: try/catch — most readable for most situations ───

async function loadUserDashboard(userId) {
  try {
    // Both of these can throw — one try/catch covers both
    const userResponse = await fetch(`https://api.example.com/users/${userId}`);

    if (!userResponse.ok) {
      // Manually throw so we catch HTTP errors, not just network errors
      throw new Error(`HTTP ${userResponse.status}: Failed to load user ${userId}`);
    }

    const user = await userResponse.json(); // This can also throw if JSON is malformed
    console.log(`Dashboard loaded for: ${user.name}`);
    return user;

  } catch (error) {
    // One place to handle every failure in this async flow
    console.error('Dashboard failed to load:', error.message);
    return null; // Return a safe fallback so callers don't blow up
  }
}

// ─── Pattern 2: The 'safeAwait' helper — great for granular error handling ───
// Instead of nested try/catches, wrap each await to get [error, result] tuples
// Inspired by Go's error handling style

async function safeAwait(promise) {
  try {
    const data = await promise;
    return [null, data]; // [error, result] — error is null on success
  } catch (error) {
    return [error, null]; // error is populated on failure
  }
}

async function processOrder(orderId) {
  // Step 1: Validate the order
  const [validationError, order] = await safeAwait(
    fetch(`https://api.example.com/orders/${orderId}`).then((r) => r.json())
  );

  if (validationError) {
    console.error('Order validation failed:', validationError.message);
    return;
  }

  // Step 2: Charge the customer — different error, different handling
  const [paymentError, receipt] = await safeAwait(
    fetch('https://api.example.com/payments', {
      method: 'POST',
      body: JSON.stringify({ orderId: order.id, amount: order.total }),
    }).then((r) => r.json())
  );

  if (paymentError) {
    // Handle payment failure specifically — maybe alert the user differently
    console.error('Payment failed — order preserved for retry:', paymentError.message);
    return;
  }

  console.log(`Order ${orderId} confirmed. Receipt ID: ${receipt.id}`);
}

// ─── Pattern 3: Propagate errors upward intentionally ───
// Sometimes you WANT the error to bubble up to the caller

async function getProductPrice(productId) {
  const response = await fetch(`https://api.example.com/products/${productId}`);
  if (!response.ok) throw new Error(`Product ${productId} not found`);
  const product = await response.json();
  return product.price;
  // No try/catch here — the caller decides how to handle the failure
}

// The caller owns the error strategy
async function renderProductPage(productId) {
  try {
    const price = await getProductPrice(productId);
    console.log(`Price: $${price}`);
  } catch (error) {
    console.log('Could not load price. Showing "Contact for pricing" instead.');
  }
}
Watch Out:
fetch() only rejects on network failure — not on 4xx or 5xx HTTP errors. Always check response.ok or response.status after awaiting a fetch call. Forgetting this is one of the most common bugs in JavaScript apps.
Production Insight
Unhandled Promise rejections cause process crashes in Node.js 15+ (exit code 1). Your app stops, no graceful shutdown.
Always catch rejections at the top level: process.on('unhandledRejection', (reason) => { console.error(reason); process.exit(1); }); in Node.js.
Rule: Every async function call should be awaited in a try/catch, or have .catch() attached. Fire-and-forget is acceptable only if errors are logged and don't affect correctness.
Key Takeaway
Always handle rejected Promises — unhandled rejections crash Node.js 15+ and are silent failures in browsers.
Use try/catch for async/await error handling — it unifies sync and async errors in one block.
Rule: fetch() does not reject on 4xx/5xx HTTP errors — always check response.ok and throw manually if needed.
async Error Handling Strategy
IfSingle async operation, need to handle errors locally
UseUse try/catch block around the await. Most readable. Exceptions caught in same function.
IfMultiple async operations, each with different error handling
UseUse safeAwait helper returning [error, result] tuples. Avoids nesting try/catch. Each operation handles its own errors inline.
IfErrors should propagate to caller (API handler, route)
UseDo NOT use try/catch. Let the exception bubble up. The caller (framework) catches and returns appropriate HTTP status.
Iffire-and-forget operation, errors should be logged but not crash
UseUse fn().catch(err => logger.error(err)). Do not use try/catch without await—it won't catch async exceptions.
Ifparallel operations with Promise.all
UseWrap Promise.all in try/catch — any rejection rejects the whole Promise.all. Use Promise.allSettled for partial tolerance.

Async Error Handling Best Practices — Try/Catch Done Right

While the previous section covered the mechanics of catching errors in async functions, this section distills the proven patterns that production code relies on. These are not theoretical — they are the result of debugging hundreds of silent failures in live applications.

Best Practice 1: Always check response.ok after fetch. The most common silent failure in JavaScript is assuming a 404 or 500 response will be caught by try/catch. It won't. fetch only rejects on network-level errors (DNS failure, connection refused). Always throw manually:

``javascript const res = await fetch(url); if (!res.ok) throw new Error(Fetch failed with status ${res.status}); ``

Best Practice 2: Wrap every await in try/catch unless you intentionally propagate. If you don't catch locally, ensure the caller has a catch. The worst state is an unhandled rejection that crashes the process.

Best Practice 3: Use Promise.allSettled for tolerant parallel operations. When one failure should not spoil the batch, allSettled gives you per-result status without throwing.

Best Practice 4: Register a global unhandled rejection handler in Node.js apps. This prevents crashes and logs unexpected rejections: ``javascript process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); // Optionally exit gracefully, but log first }); ``

Best Practice 5: Avoid mixing .catch() and try/catch on the same Promise. Pick one pattern per async block to avoid confusing control flow.

io/thecodeforge/js/asyncErrorBestPractices.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ─── Best Practice 1: always check response.ok ───
async function fetchUserSafe(userId) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    // This is NOT caught by try/catch unless we throw
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }
  return response.json();
}

// ─── Best Practice 2: wrap in try/catch (or propagate with .catch) ───
async function safeUserLoad(userId) {
  try {
    const user = await fetchUserSafe(userId);
    return user;
  } catch (err) {
    console.error('User load failed:', err.message);
    return null; // return fallback
  }
}

// ─── Best Practice 3: Promise.allSettled for tolerant parallel ops ───
async function loadDashboard(userId) {
  const results = await Promise.allSettled([
    fetchUserSafe(userId),
    fetchOrders(userId),
    fetchNotifications(userId),
  ]);

  // Filter successes, log failures
  const successData = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  results
    .filter(r => r.status === 'rejected')
    .forEach(r => console.warn('A call failed:', r.reason));

  return successData;
}

// ─── Best Practice 5: consistent error strategy ───
// Don't mix .catch() and try/catch on same promise chain
// Bad:
// const data = await fetch(url).catch(err => handle(err));
// try { data; } catch (err) {} // Confusing, may miss errors

// Good: pick one
const data = await fetch(url).catch(err => {
  console.error(err);
  return null;
});
Production Gotcha:
In Node.js 15+, an unhandled Promise rejection will terminate the process with a non-zero exit code. This is designed to prevent silent data loss. Always add a global handler or ensure every async call is awaited and caught.
Production Insight
The most effective error handling strategy is defense in depth: catch at the call site, log structured errors, and have a global handler as a safety net. In Express.js, wrap async route handlers with a catch-all that passes to error middleware. Never assume an async operation will succeed — always plan for failure.
Key Takeaway
Every async call must have an error handler — either a try/catch block or a .catch() attached. The response.ok check after fetch is non-negotiable for production reliability.

Sequential vs Parallel Execution — The Performance Trap

This is the mistake that separates developers who understand async from those who just use it. When you write multiple await statements one after another, they run sequentially — each one waits for the previous to finish before starting. For operations that are independent of each other, this is a massive and completely unnecessary performance hit.

Imagine you need a user's profile, their recent orders, and their notification count to render a dashboard. None of those three requests depends on the others. Running them sequentially means if each takes 300ms, your dashboard takes 900ms. Running them in parallel means it takes 300ms — the time of the slowest one.

Promise.all() is your tool here. It fires all Promises at once and waits for all of them to resolve, giving you a big performance win for independent operations. Use Promise.allSettled() when you want all results regardless of whether some fail — it never rejects, it gives you an array of outcome objects.

Promise.race() resolves or rejects as soon as the first Promise settles — great for implementing timeouts. Promise.any() resolves with the first success and only rejects if all fail — useful for redundant data sources like CDN fallbacks.

io/thecodeforge/js/parallelVsSequential.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// ─── The slow way: Sequential (bad for independent operations) ───

async function loadDashboardSlowly(userId) {
  console.time('sequential');

  // Each await WAITS for the previous one to finish.
  // Total time = 400ms + 350ms + 300ms = 1050ms
  const userProfile = await fetchUserProfile(userId);       // 400ms
  const recentOrders = await fetchRecentOrders(userId);     // 350ms  
  const notifications = await fetchNotifications(userId);   // 300ms

  console.timeEnd('sequential'); // ~1050ms
  return { userProfile, recentOrders, notifications };
}

// ─── The fast way: Parallel with Promise.all() ───

async function loadDashboardFast(userId) {
  console.time('parallel');

  // All three fetches START at the same time.
  // Total time ≈ max(400ms, 350ms, 300ms) = 400ms
  const [userProfile, recentOrders, notifications] = await Promise.all([
    fetchUserProfile(userId),    // Fires immediately
    fetchRecentOrders(userId),   // Also fires immediately
    fetchNotifications(userId),  // Also fires immediately
  ]);

  // Note: if ANY of these rejects, the whole Promise.all rejects.
  // Use Promise.allSettled if you want partial results on failure.

  console.timeEnd('parallel'); // ~400ms
  return { userProfile, recentOrders, notifications };
}

// ─── When some requests CAN fail without breaking everything ───

async function loadDashboardResilient(userId) {
  const results = await Promise.allSettled([
    fetchUserProfile(userId),
    fetchRecentOrders(userId),
    fetchNotifications(userId),
  ]);

  // allSettled never throws — each result has a status
  const [profileResult, ordersResult, notificationsResult] = results;

  const dashboard = {
    // Show user profile if loaded, otherwise show a fallback
    user: profileResult.status === 'fulfilled'
      ? profileResult.value
      : { name: 'Guest', role: 'unknown' },

    // Show orders if loaded, otherwise show empty state
    orders: ordersResult.status === 'fulfilled'
      ? ordersResult.value
      : [],

    // Notification count can silently fail — not critical
    notificationCount: notificationsResult.status === 'fulfilled'
      ? notificationsResult.value.length
      : 0,
  };

  if (ordersResult.status === 'rejected') {
    console.warn('Orders failed to load:', ordersResult.reason.message);
  }

  return dashboard;
}

// ─── Mock fetch helpers to make this runnable ───
function fetchUserProfile(userId) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ id: userId, name: 'Alex Rivera', role: 'admin' }), 400)
  );
}
function fetchRecentOrders(userId) {
  return new Promise((resolve) =>
    setTimeout(() => resolve([{ id: 'ORD-001', total: 89.99 }]), 350)
  );
}
function fetchNotifications(userId) {
  return new Promise((resolve) =>
    setTimeout(() => resolve([{ msg: 'Your order shipped' }, { msg: 'Flash sale today' }]), 300)
  );
}

// Run the fast version
loadDashboardFast(7).then((data) => {
  console.log('User:', data.userProfile.name);
  console.log('Orders:', data.recentOrders.length);
  console.log('Notifications:', data.notifications.length);
});
Pro Tip:
A quick way to audit your own async code: scan for multiple awaits in a row. Ask yourself 'does request B need request A's result to run?' If the answer is no, those awaits should be inside Promise.all(). This single habit can cut API-heavy page load times by 50-70%.
Production Insight
Promise.all rejects immediately if any Promise rejects. This fails fast, which is good for critical operations but loses partial data.
Promise.allSettled waits for all Promises and reports each status. Use it for non-critical operations where partial data is acceptable.
Rule: For independent API calls, use Promise.all (or allSettled). For dependent calls (second needs result of first), use sequential await.
Key Takeaway
Multiple consecutive awaits on independent operations is a performance bug — use Promise.all() to run independent async operations in parallel and cut total wait time to the slowest single request.
Promise.all rejects on first failure; Promise.allSettled waits for all and reports success/failure per operation.
Rule: Audit your code for awaits in a row. If they don't depend on each other, parallelise them.
Parallel vs Sequential Decision Tree
IfOperations are independent and all must succeed for the operation to succeed
UseUse const [a, b] = await Promise.all([op1(), op2()]). Fastest, fails fast on any error.
IfOperations are independent but some may fail; we want partial results
UseUse const results = await Promise.allSettled([op1(), op2()]). Filter results.filter(r => r.status === 'fulfilled').
IfOperation B depends on result of Operation A
UseUse sequential await: const a = await op1(); const b = await op2(a);. Cannot parallelise.
IfLarge number of independent operations (100+), need concurrency control
UseUse p-limit or manual batching: split into chunks of 10, await Promise.all on each chunk, add delay between chunks.
IfNeed fastest response from multiple redundant sources (CDN fallback)
UseUse Promise.any([source1(), source2()]). Resolves with first successful Promise. Rejects only if all fail.

Sequential vs Parallel Execution — Performance Comparison

To make the performance impact concrete, here is a side‑by‑side comparison of sequential (await in a for...of loop) versus parallel (Promise.all) execution for realistic workloads. The numbers assume independent operations with the given latencies.

ScenarioSequentialParallelSpeedup
3 API calls, each 300ms900ms~300ms3x
10 API calls, each 200ms2,000ms~200ms10x
100 DB queries, each 50ms5,000ms~50ms100x
Mixed: 300ms, 200ms, 400ms900ms400ms2.25x
5 calls: 100ms each500ms100ms5x

The pattern is clear: the more independent operations you have, the more dramatic the savings. For 100 operations, sequential takes 5 seconds; parallel finishes in 50ms — a 100x improvement.

However, there are two critical caveats: 1. Concurrency limits: Firing 100 requests in parallel may overwhelm the target API or your own network. Use batching (see the Concurrency Control section) when you have more than 10–20 operations. 2. Dependency chain: If operation B needs the result of A, you cannot parallelise. Sequential is required.

Always measure with realistic latencies — network jitter means the slowest call in a parallel batch may be an outlier, making total time longer than the average max.

io/thecodeforge/js/seqVsParComparison.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ─── Benchmark sequential vs parallel ───

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function sequentialExample(items) {
  const results = [];
  for (const item of items) {
    await delay(100); // simulate async work
    results.push(item * 2);
  }
  return results;
}

async function parallelExample(items) {
  const promises = items.map(async (item) => {
    await delay(100);
    return item * 2;
  });
  return Promise.all(promises);
}

// Simulate 5 items, each taking 100ms
const items = [1, 2, 3, 4, 5];

console.time('sequential');
await sequentialExample(items);
console.timeEnd('sequential'); // ~500ms

console.time('parallel');
await parallelExample(items);
console.timeEnd('parallel');   // ~100ms
Benchmark Reality Check:
In production, the slowest operation in a Promise.all batch determines total time. Use tools like console.time() or APM to measure actual latencies. The speedup is often less than theoretical due to resource contention, but still significant.
Production Insight
When deploying code that switches from sequential to parallel, monitor the downstream API's response times and error rates. A sudden flood of parallel requests can trigger rate limiting or throttling. Always introduce parallelism gradually (e.g., start with Promise.all on a few calls, then expand) and have circuit breakers ready.
Key Takeaway
For independent async operations, parallel execution (Promise.all) is orders of magnitude faster than sequential loops. The speedup ratio equals the number of operations, minus overhead. Always benchmark with real latencies.

Real-World Pattern — Building a Resilient API Service

Knowing the mechanics is one thing. Knowing how to structure async code in a real application is another. Most production JavaScript — whether Node.js servers or React apps — wraps its async logic in service layers: plain modules that own all the fetch/database logic and export clean async functions.

This pattern keeps your components or route handlers lean. They just call await userService.getProfile(id) and handle the result. The async complexity is encapsulated in one place.

Let's also add a timeout pattern, because every real API call needs one. You can't let a slow third-party API hang your server forever. Promise.race() between your actual request and a timeout Promise is the classic solution.

Finally, adding a retry mechanism for transient failures is something every production app needs. A request that fails once due to a brief network hiccup should retry before giving up. These three ideas — service layer, timeout, and retry — are the building blocks of resilient async code.

io/thecodeforge/js/resilientApiService.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// ─── A production-quality async service with timeout and retry ───

// Utility: Rejects after `ms` milliseconds — used for timeouts
function createTimeoutPromise(ms, operationName) {
  return new Promise((_, reject) =>
    setTimeout(
      () => reject(new Error(`${operationName} timed out after ${ms}ms`)),
      ms
    )
  );
}

// Utility: Retries a Promise-returning function up to `maxAttempts` times
async function withRetry(asyncFn, maxAttempts = 3, delayMs = 500) {
  let lastError;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await asyncFn(); // Try running the function
    } catch (error) {
      lastError = error;

      const isLastAttempt = attempt === maxAttempts;
      if (isLastAttempt) break;

      console.warn(`Attempt ${attempt} failed. Retrying in ${delayMs}ms...`);
      // Wait before retrying — prevents hammering a struggling server
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }

  throw lastError; // All retries exhausted — propagate the final error
}

// ─── The actual service layer ───
const WEATHER_API_TIMEOUT_MS = 5000;

async function fetchWeatherData(cityName) {
  // Race: the real request vs a timeout — whoever settles first wins
  const weatherPromise = fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=DEMO_KEY`
  ).then((response) => {
    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status} for city '${cityName}'`);
    }
    return response.json();
  });

  return Promise.race([
    weatherPromise,
    createTimeoutPromise(WEATHER_API_TIMEOUT_MS, `fetchWeatherData(${cityName})`),
  ]);
}

// Public API of our weather service — this is what callers use
const weatherService = {
  async getWeather(cityName) {
    // Wrap the fetch in our retry utility — handles transient failures
    const rawData = await withRetry(
      () => fetchWeatherData(cityName),
      3,   // up to 3 attempts
      800  // 800ms between retries
    );

    // Transform the raw API response into a clean shape our app cares about
    return {
      city: rawData.name,
      country: rawData.sys.country,
      temperatureCelsius: Math.round(rawData.main.temp - 273.15),
      description: rawData.weather[0].description,
      humidity: rawData.main.humidity,
      fetchedAt: new Date().toISOString(),
    };
  },

  async getWeatherForMultipleCities(cityNames) {
    // Fetch all cities in parallel — don't make users wait for each one
    const weatherResults = await Promise.allSettled(
      cityNames.map((city) => this.getWeather(city))
    );

    return weatherResults.map((result, index) => ({
      city: cityNames[index],
      success: result.status === 'fulfilled',
      data: result.status === 'fulfilled' ? result.value : null,
      error: result.status === 'rejected' ? result.reason.message : null,
    }));
  },
};

// ─── Usage — clean and readable because the complexity is hidden ───
async function renderWeatherDashboard() {
  const cities = ['London', 'Tokyo', 'New York'];

  console.log(`Fetching weather for: ${cities.join(', ')}`);

  const results = await weatherService.getWeatherForMultipleCities(cities);

  results.forEach(({ city, success, data, error }) => {
    if (success) {
      console.log(`${city}: ${data.temperatureCelsius}°C, ${data.description}`);
    } else {
      console.log(`${city}: Failed to load — ${error}`);
    }
  });
}

renderWeatherDashboard();
Architecture Note:
The withRetry and createTimeoutPromise utilities here are framework-agnostic — drop them into any Node.js, React, or vanilla JavaScript project. Libraries like axios-retry do similar things, but knowing how to build it yourself means you can customize behavior (exponential backoff, retry only on specific status codes) instead of being limited by someone else's API.
Production Insight
Promises are eager — they start executing as soon as you call them, even before you await.
This means const promise = fetch(url); starts the request immediately. await promise waits for it to finish.
Rule: For Promise.all, create all Promises first (they run in parallel) then await Promise.all. Do NOT await inside the array creation.
Key Takeaway
Service layers encapsulate async complexity — retries, timeouts, error handling, and response transformation.
Promise.race with a timeout prevents hanging requests from blocking the server indefinitely.
Rule: For production APIs, implement timeout, retry, and circuit breaker patterns. Never trust external services to respond quickly or correctly.
Async Service Design Patterns
IfAPI client (fetch wrapper) for external service
UseEncapsulate fetch logic, response.ok check, and JSON parsing. Export async functions that return typed data. Add timeout and retry logic.
IfDatabase repository
UseExport async methods that use db client's promise API. Use connection pooling. Handle database-specific errors (duplicate key, constraint violation).
IfParallel data fetching for dashboard
UseUse Promise.allSettled with mapping from fields to fetch functions. Return partial results; UI shows fallback for failed sections.
IfRate-limited API batching
UseSplit requests into chunks (size = rate limit). Process each chunk with Promise.all, add delay between chunks using setTimeout promisified.
IfLong-polling or streaming
UseUse for loop with await inside. Not Promise.all. Recursively fetch next chunk until done. Track cancellation with AbortController.

Concurrency Control with Batching — Fire-and-Forget Pitfall Fixed

When you have a large number of independent async operations (e.g., sending 1000 emails, updating 500 database records, or calling a third-party API with rate limits), neither sequential nor raw parallel execution works well:

  • Sequential: Too slow — each operation waits for the previous one to finish.
  • Raw Promise.all: Fires all operations at once — overwhelms downstream services, triggers rate limits, and may exhaust memory/file handles on your own server.

The solution is **batching**: split the items into chunks, process each chunk in parallel (using Promise.all on the chunk), and add a small delay between chunks to respect rate limits.

This pattern gives you the throughput of parallelism with the safety of controlled concurrency. The chunk size should be tuned to the API's rate limit (e.g., 50 requests per second means chunk size 50 with a 1-second delay between chunks). If the API has no explicit limit, start with chunk size 10 and increase gradually while monitoring error rates.

Below is a reusable batchProcess function that you can drop into any project. It accepts an array, an async processing function, the desired concurrency (chunk size), and an optional delay between chunks.

io/thecodeforge/js/batchProcess.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * Processes an array of items in batches.
 * Each batch runs its items in parallel (Promise.all).
 * A configurable delay is inserted between batches to respect rate limits.
 *
 * @param {Array} items - The items to process.
 * @param {Function} asyncFn - An async function that processes a single item.
 * @param {number} concurrency - Number of items to process in parallel per batch.
 * @param {number} delayMs - Delay (ms) between batches. Default 0.
 * @param {boolean} useSettled - If true, use Promise.allSettled; if false, Promise.all.
 * @returns {Promise<Array>} Results from all items, in order.
 */
async function batchProcess(items, asyncFn, concurrency, delayMs = 0, useSettled = false) {
  const results = [];
  const batches = [];

  // Split items into chunks
  for (let i = 0; i < items.length; i += concurrency) {
    batches.push(items.slice(i, i + concurrency));
  }

  for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
    const batch = batches[batchIndex];
    console.log(`Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} items)`);

    // Map each item to the async function (promises start immediately)
    const promises = batch.map((item, idx) => asyncFn(item, batchIndex * concurrency + idx));

    let batchResults;
    if (useSettled) {
      batchResults = await Promise.allSettled(promises);
    } else {
      batchResults = await Promise.all(promises);
    }

    results.push(...batchResults);

    // Delay between batches (except after the last one)
    if (delayMs > 0 && batchIndex < batches.length - 1) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  return results;
}

// ─── Example: Processing 100 payment IDs with concurrency 20 and 500ms delay ───

async function processPayment(paymentId) {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 200));
  return { id: paymentId, status: 'paid' };
}

const paymentIds = Array.from({ length: 100 }, (_, i) => `PAY-${i + 1}`);

const allResults = await batchProcess(
  paymentIds,
  processPayment,
  20,    // concurrency: 20 simultaneous calls
  500,   // 500ms delay between batches
  true   // use allSettled so one failure doesn't kill the batch
);

const succeeded = allResults.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = allResults.filter(r => r.status === 'rejected').map(r => r.reason);
console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
Picking a Chunk Size:
Start with a conservative chunk size (e.g., 10) and increase while monitoring the downstream service's response times and error rates. If you start seeing 429 (Too Many Requests) or timeouts, reduce the chunk size or increase the delay. For services with documented rate limits, set concurrency = rate limit and delay = 1000ms for a limit of 1 request per second per token, or concurrency = limit and delay = 1000 if the limit is per second.
Production Insight
Batching is not just for rate limiting — it also helps manage resource usage on your own server. Each concurrent async operation may hold a socket, a database connection, or memory. Without batching, 1000 concurrent operations can exhaust file descriptors or cause memory pressure. Always set an upper bound on concurrency, even if the downstream service has no rate limit. Use tools like p-limit (npm) for a more feature-rich concurrency limiter, but the manual batch pattern is more transparent and easier to debug.
Key Takeaway
For large numbers of independent async operations, use batching: process items in parallel chunks with delays between chunks to respect rate limits and manage resource usage. Tune chunk size based on downstream capacity.

Async Function Hoisting — The Gotcha That Wastes Hours in Debugging

You think you understand hoisting? Good. async functions hoist differently than function expressions, and it bites teams weekly. An async function declared with the function keyword is hoisted to the top of its scope — you can call it before its definition in the file. But assign that same async behavior to a const or let, and you get a ReferenceError if you access it before the assignment. This isn't academic. I've traced production outages where a developer refactored an async function declaration into an arrow function for "consistency" and the entire module broke silently. Here's why it matters: async functions return a Promise. If you accidentally invoke a hoisted version before its definition, you get undefined — not an error — and your downstream Promise.all waits forever. The fix? Never rely on hoisting for async functions. Declare them with const and place them at the top of your module. Your future self won't hate you.

AsyncHoistingPitfall.javascriptJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — javascript tutorial

// This works — async function declaration is hoisted
await fetchUserData(42);

async function fetchUserData(userId) {
  // Simulate API call
  return { id: userId, name: 'Alice' };
}

// This blows up — const is NOT hoisted
await fetchOrderHistory(42);
// ReferenceError: Cannot access 'fetchOrderHistory' before initialization

const fetchOrderHistory = async (userId) => {
  return { userId, orders: [] };
};
Output
// Works: { id: 42, name: 'Alice' }
// Fails: ReferenceError: Cannot access 'fetchOrderHistory' before initialization
Never Do This:
Refactoring an async function declaration to an arrow function without moving it above all call sites. The hoisting difference is invisible until runtime.
Key Takeaway
Always use const for async functions and declare them before any code that calls them. Hoisting is a trap, not a feature.

Rewriting Promise Chains with async/await — Don't Copy, Refactor

Most tutorials show you how to mechanically replace .then() with await. That's not refactoring — that's search-and-replace. Real async/await rewrites fix the broken error handling and control flow you tolerated in promise chains. Here's the pattern you see daily: a .then() chain with a .catch() at the end that swallows every error into a single black hole. When you convert that to async/await, don't just wrap the whole thing in a single try/catch. That replicates the same broken behavior. Instead, scope your try/catch blocks around individual async operations that can fail independently. The production benefit? When a third-party API fails, you retry just that call — not replay an entire chain of side effects. I've seen a ticket queue spike because someone "refactored" a promise chain with three independent API calls into one huge try/catch that retried everything on any failure. Three times the latency. Three times the bills. Think about what can fail independently, and isolate your error handling there.

RefactorPromiseChain.javascriptJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io.thecodeforge — javascript tutorial

// BEFORE: monolithic promise chain, error swallowed at the end
function loadDashboardLegacy(userId) {
  return fetchUser(userId)
    .then(user => fetchUserPosts(user.id))
    .then(posts => enrichPostsWithComments(posts))
    .catch(err => {
      console.error('Dashboard failed:', err);
      return null; // Everything fails or nothing
    });
}

// AFTER: scoped error handling, independent retry logic
async function loadDashboardRefactored(userId) {
  let user;
  try {
    user = await fetchUser(userId);
  } catch (err) {
    console.error('User fetch failed, aborting:', err);
    return null;
  }
  
  let posts;
  try {
    posts = await fetchUserPosts(user.id);
  } catch (err) {
    console.error('Posts fetch failed, showing empty dashboard:', err);
    posts = []; // Degrade gracefully, don't crash the whole thing
  }
  
  try {
    posts = await enrichPostsWithComments(posts);
  } catch (err) {
    console.warn('Comment enrichment failed, showing posts only:', err);
  }
  
  return { user, posts };
}
Output
// On user fetch failure: logged, returns null
// On posts fetch failure: logged, shows empty posts array
// On enrichment failure: logged, shows posts without comments
Senior Shortcut:
When rewriting a promise chain, first identify each operation that could be retried or skipped independently. That's where your async/await try/catch boundaries should go.
Key Takeaway
Granular try/catch blocks in async/await let you fail fast on critical operations and degrade gracefully on non-critical ones — something monolithic .catch() chains can't do.
● Production incidentPOST-MORTEMseverity: high

The Async forEach That Crashed the Payment Processor

Symptom
The batch job logged 'Processed 1000 orders' within 2 seconds, but accounting showed only ~300 payments succeeded. No errors in logs. The API rate limit was 100 requests/second, and 1000 simultaneous requests overwhelmed the payment gateway, causing 700 rejections. The forEach loop never waited for the Promises, so the script simply exited after initiating all requests—most were still pending.
Assumption
The developer assumed forEach would await each iteration sequentially. They didn't know that forEach ignores the return value of the callback, and async functions always return a Promise. They also assumed the payment gateway could handle any request rate — it couldn't. The team had tested with 10 orders, all succeeded, but failed with 1000.
Root cause
orderIds.forEach(async id => { await processPayment(id); }) — forEach calls the callback for each element but does NOT await the returned Promise. All 1000 payment requests fired simultaneously, overloading the payment gateway's rate limit. The script moved to the next line after firing all requests, logging 'Processed 1000 orders' before any payment actually completed. Errors from the rejected payments were unhandled because no .catch() was attached to the returned Promises. The team never saw the 700 errors.
Fix
1. Replaced forEach with for (const id of orderIds) { await processPayment(id); } for sequential processing (2 seconds per payment = 33 minutes for 1000 orders—unacceptable). 2. Used proper batching: split orders into chunks of 50, process each chunk with Promise.all(chunk.map(processPayment)), with a 500ms delay between chunks to respect rate limits. 3. Added Promise.allSettled to capture all successes and failures without early rejection. 4. Wrapped the entire batch in try-catch and logged result counts (success/failure). 5. Added a circuit breaker to stop processing if failure rate exceeds 10%.
Key lesson
  • forEach does NOT wait for async callbacks. It fires all and forgets. Use for...of with await for sequential, or Promise.all with .map for controlled concurrency.
  • Always handle Promise rejections. Unhandled rejections crash Node.js 15+ and are silent failures in browsers.
  • Test async code with realistic concurrency levels, not just single-item success cases.
  • Use batching with Promise.all on chunks to balance throughput and load. Chunk size = 10-100 depending on API limits.
Production debug guideSymptom → Action mapping for common async failures in production JS apps.5 entries
Symptom · 01
forEach loop runs but operations never complete — script exits early
Fix
forEach doesn't await Promises. Replace with for (const item of arr) { await fn(item) } for sequential, or await Promise.all(arr.map(fn)) for parallel with controlled concurrency.
Symptom · 02
API calls failing silently — no errors, no data
Fix
fetch() only rejects on network errors, not HTTP 4xx/5xx. Always check response.ok and throw manually: if (!res.ok) throw new Error(${res.status}). Also check for unhandled rejections: add process.on('unhandledRejection', console.error) in Node.js.
Symptom · 03
React component re-renders before data loads — flashes 'undefined'
Fix
State starts as empty array/object. Set initial state to loading flag: const [data, setData] = useState(null); if (!data) return <Loader />. Await in useEffect with async function inside.
Symptom · 04
Slow page loads — multiple independent API calls are sequential
Fix
You're awaiting each call separately: const a = await fetchA(); const b = await fetchB();. Use Promise.all([fetchA(), fetchB()]) to parallelise independent requests. For large numbers, use Promise.allSettled.
Symptom · 05
try/catch doesn't catch error from async function
Fix
You forgot await. If you call asyncFn() without await, the Promise is returned but not awaited. The try/catch only catches sync errors. Add await: await asyncFn(). Or use .catch() on the Promise.
★ async/await Debug Cheat SheetFast diagnostics for async issues in production JavaScript applications.
forEach not awaiting — code runs before async work finishes
Immediate action
Check if forEach is used with async callback
Commands
grep -n 'forEach.*async' src/**/*.js
grep -n 'await.*forEach' src/**/*.js
Fix now
Replace arr.forEach(async x => await fn(x)) with for (const x of arr) { await fn(x); } or await Promise.all(arr.map(fn))
fetch errors not caught — silent failures+
Immediate action
Check fetch calls for response.ok handling
Commands
grep -n 'fetch' src/**/*.js | grep -v 'response.ok'
grep -n '.catch' src/**/*.js | grep -v 'fetch'
Fix now
Add if (!response.ok) throw new Error(HTTP ${response.status}) after fetch. Register global unhandled rejection handler.
React component showing undefined before data loads+
Immediate action
Check if component renders before async data is ready
Commands
grep -n 'useState.*null' src/components/*.jsx
grep -n 'await.*useEffect' src/components/*.jsx
Fix now
Initialize state with null or empty array, render loading indicator while data is null. Use if (!data) return <Spinner />
Promise.all fails early on first rejection — some data lost+
Immediate action
Check if Promise.all used without error tolerance
Commands
grep -n 'Promise.all' src/**/*.js
grep -n 'Promise.allSettled' src/**/*.js
Fix now
Replace Promise.all with Promise.allSettled when you need partial results. Filter results: results.filter(r => r.status === 'fulfilled').map(r => r.value)
unhandledrejection event firing — process crash or silent fail+
Immediate action
Check for missing .catch() on Promise or await in try/catch
Commands
node --unhandled-rejections=strict app.js
grep -n '.catch' src/**/*.js | wc -l
Fix now
Wrap await in try/catch: try { await asyncOp() } catch (e) { console.error(e) }. Add global handler: process.on('unhandledRejection', console.error) in Node.js.
async/await vs Promise.then vs Callbacks
Feature / AspectPromise .then()/.catch()async / awaitCallbacks
Readability for simple sequential flowsChains get noisy with multiple stepsReads like synchronous code — linear and cleanPyramid of doom for nested dependencies
Error handling.catch() at the end of a chaintry/catch blocks — same syntax as sync errorsEach callback must check error param; easy to miss
Debugging stack tracesStack traces often lose call context across .then()Stack traces are more accurate and readableCall stack is lost; async context difficult
Conditional async logicNested .then() chains, hard to readSimple if/else inside the async functionif/else with callbacks -> nesting
Parallel executionPromise.all() directlyawait Promise.all([...]) — cleaner syntaxManual counter tracking (let remaining = 2)
Learning curveMust understand Promise constructor and chainingRequires understanding Promises but less mental overheadLow entry, high pain for complex flows
Top-level usageWorks anywhereRequires async function wrapper (or top-level await in modules)Works anywhere
Cancellation supportAbortController + fetch signalSame — pass signal to fetch and abortManual (clearTimeout, etc.)
Adoption in 2026Legacy code, library internalsStandard for new developmentLegacy Node.js APIs only

Key takeaways

1
async functions always return a Promise
even if you return a plain value like a string or number, it gets wrapped in Promise.resolve() automatically.
2
await only blocks the current async function's execution, not the JavaScript thread
the event loop continues running other code while it waits.
3
Multiple consecutive awaits on independent operations is a performance bug
use Promise.all() to run independent async operations in parallel and cut total wait time to the slowest single request.
4
fetch() does NOT reject on 4xx/5xx HTTP errors
you must manually check response.ok or response.status after every await fetch(...) call, or you'll silently miss server errors.
5
forEach does NOT await async callbacks
it fires all in parallel and moves on. Use for...of with await for sequential, or Promise.all with .map for parallel with completion tracking.

Common mistakes to avoid

5 patterns
×

Using forEach with async callbacks expecting sequential execution

Symptom
forEach runs all iterations in parallel, the outer code continues before any async work finishes. No errors, just missing data. The loop may finish before any callback completes.
Fix
Use for (const item of items) { await fn(item); } for sequential execution. Use await Promise.all(items.map(fn)) for parallel with concurrency control.
×

Forgetting to check response.ok in fetch calls

Symptom
fetch resolves even on HTTP 404, 500. The code proceeds with response.json() which may reject with invalid JSON (empty body) or succeed with error body. No error is raised for the bad status.
Fix
Always add if (!response.ok) throw new Error(HTTP ${response.status}) after fetch. Or use a wrapper function that does this automatically.
×

Unhandled Promise rejections crashing Node.js

Symptom
Node.js 15+ exits with code 1 when a Promise rejection is not handled. In browsers, the rejection is silent in the console but doesn't crash.
Fix
Add global handler: process.on('unhandledRejection', console.error) in Node.js. In code, wrap every await in try/catch or attach .catch() to every Promise chain that may reject.
×

Using Promise.all when some operations can fail and you need partial results

Symptom
Promise.all rejects immediately on first failure, losing all other successful results. Components that could render partially instead show nothing.
Fix
Use Promise.allSettled instead. It waits for all Promises and provides status for each: const results = await Promise.allSettled(promises); then filter results.filter(r => r.status === 'fulfilled').map(r => r.value).
×

Awaiting inside the Promise.all array creation

Symptom
await Promise.all([await slowOp1(), slowOp2()]) — the second await inside the array causes sequential execution because slowOp1 is awaited before the array is created.
Fix
Create Promises without awaiting: const p1 = slowOp1(); const p2 = slowOp2(); await Promise.all([p1, p2]);. The operations start immediately, then you wait for all to complete.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between Promise.all() and Promise.allSettled()? W...
Q02SENIOR
If you have three independent API calls in an async function and you wri...
Q03SENIOR
Can you use await outside of an async function? What happens if you try,...
Q04SENIOR
Why does the following code not work as expected? `[1, 2, 3].forEach(asy...
Q01 of 04SENIOR

What is the difference between Promise.all() and Promise.allSettled()? When would you choose one over the other?

ANSWER
Promise.all() resolves when all input Promises resolve, or rejects immediately when any Promise rejects (fail-fast). It returns an array of resolved values in order. Promise.allSettled() waits for all Promises to settle (resolve or reject) and never rejects — it returns an array of objects each with status: 'fulfilled' or 'rejected' and value or reason. Choose Promise.all when all operations must succeed for the result to be useful (e.g., loading critical user data before rendering page). Choose Promise.allSettled when partial results are acceptable, and you want to display success for some and errors for others (e.g., dashboard with multiple widgets each fetching independently). allSettled is better for resilience; all is better for atomic operations.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I use async await with forEach in JavaScript?
02
What's the difference between async await and Promises in JavaScript?
03
Does await block the entire JavaScript program?
04
How do I cancel an async operation in JavaScript?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Advanced JS. Mark it forged?

9 min read · try the examples if you haven't

Previous
Promises in JavaScript
3 / 27 · Advanced JS
Next
Event Loop in JavaScript