JavaScript Async/Await Patterns That Trip Up Senior Devs

JavaScript Async/Await Patterns That Trip Up Senior Devs
Brandon Perfetti

Technical PM + Software Engineer

Topics:Error HandlingPerformance
Tech:JavaScriptTypeScriptNode.js

Async/await made JavaScript more readable, but it also made subtle failure modes easier to hide.

Most production incidents in async code are not syntax problems. They are policy problems:

  • what is required vs optional,
  • what should run in parallel,
  • what can be retried safely,
  • what must be cancelled,
  • and what errors must retain context.

This article focuses on production-grade patterns that prevent the failures senior teams still run into.

Pattern 1: Separate required and optional concurrency

Promise.all is correct only when all results are required.

If partial data is acceptable, use Promise.allSettled and encode policy explicitly.

const [profileR, activityR, recsR] = await Promise.allSettled([
  fetchProfile(userId),
  fetchActivity(userId),
  fetchRecommendations(userId),
]);

if (profileR.status !== "fulfilled") throw new Error("profile unavailable");
if (activityR.status !== "fulfilled") throw new Error("activity unavailable");
const recommendations = recsR.status === "fulfilled" ? recsR.value : [];

In plain English: make degradation policy visible in code, not implied by accident.

Pattern 2: Avoid accidental serialization

Sequential await on independent calls adds avoidable latency.

const flagsP = getFlags(userId);
const permsP = getPermissions(userId);
const prefsP = getPreferences(userId);

const [flags, perms, prefs] = await Promise.all([flagsP, permsP, prefsP]);

Rule: start independent work first, then await together.

Pattern 3: Never use async forEach

forEach does not await callback promises.

Use:

  • for...of for ordered/limited work,
  • bounded-concurrency worker pools for bulk tasks.
for (const item of items) {
  await processItem(item);
}

Or bounded parallelism with explicit limits when throughput matters.

Pattern 4: Propagate cancellation

Async work should stop when callers no longer need results.

Use AbortController and pass AbortSignal through transport boundaries.

async function fetchBundle(id: string, signal: AbortSignal) {
  const [a, b] = await Promise.all([
    fetch(`/api/a/${id}`, { signal }).then(r => r.json()),
    fetch(`/api/b/${id}`, { signal }).then(r => r.json()),
  ]);
  return { a, b };
}

Without cancellation, you pay for stale work and risk writing outdated results.

Pattern 5: Preserve error context

Do not replace rich failures with generic errors.

Bad:

catch {
  throw new Error("request failed");
}

Better:

catch (err) {
  throw new Error("request failed", { cause: err as Error });
}

Add stable error codes at domain boundaries for consistent handling.

Pattern 6: Retry only retryable work

Retries without policy create duplicate side effects.

Before retrying, answer:

  1. Is operation idempotent?
  2. Is failure class retryable?
  3. Is backoff/jitter applied?

Use idempotency keys for write operations with external effects.

Pattern 7: Keep transport and domain concerns separate

Layer async code:

  • transport: timeout/retry/cancellation,
  • orchestration: sequencing/parallelism,
  • domain: business decisions and user-facing behavior.

Mixing these layers makes code hard to test and easy to break.

Pattern 8: Define async contracts explicitly

For high-impact async functions, document:

  • thrown error families,
  • retryability,
  • idempotency,
  • cancellation support.

If callers guess these rules, behavior will drift.

Sanity checks for async code

Before you ship:

  1. Independent work is parallelized intentionally.
  2. Required vs optional outcomes are explicit.
  3. Cancellation is propagated where applicable.
  4. No async forEach.
  5. Errors preserve causal context.
  6. Retry policy is idempotency-aware.
  7. Contract assumptions are documented.

Closing

Async/await is not inherently risky. Implicit control-flow policy is.

When concurrency, cancellation, degradation, and retries are explicit, async systems become easier to reason about and significantly more reliable under production pressure.