Advanced Async and Concurrency Patterns in JavaScript

🟦 JavaScript Deep Dive β€” Issue #5

Beyond Promises: Advanced Async & Concurrency Patterns in JavaScript

How modern JS handles real-world async workloads at scale

Promises and async/await made JavaScript easier to read β€” but they didn’t magically solve concurrency, coordination, cancellation, or backpressure.

As apps grow more complex (AI calls, streaming APIs, real-time UIs, background sync), developers quickly run into async problems that Promises alone can’t solve cleanly.

This issue explores advanced async patterns that professional JavaScript developers rely on to build reliable, scalable systems.


🧠 Why Async Gets Hard at Scale

Simple async flows look great:

const data = await fetchData();

But real-world apps deal with:

  • Multiple async tasks running at once
  • Partial failures
  • Cancellation
  • Timeouts
  • Rate limits
  • Streaming data
  • Background processing
  • Shared resources

This is where advanced async patterns matter.


🟨 1. Parallel vs Sequential Async (Know the Difference)

❌ Accidental sequential execution

for (const item of items) {
  await process(item);
}

This runs one at a time β€” slow and often unnecessary.

βœ… Intentional parallel execution

await Promise.all(items.map(process));

Runs tasks concurrently.

⚠️ But be careful

Unbounded concurrency can overload:

  • APIs
  • Browsers
  • Memory

Which leads us to…


🟨 2. Concurrency Limits (Async Pools)

Limit how many async tasks run at once.

async function asyncPool(limit, items, fn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const p = Promise.resolve().then(() => fn(item));
    results.push(p);

    if (limit <= items.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

  return Promise.all(results);
}

Used in:

  • API batching
  • Image processing
  • File uploads
  • AI inference pipelines

🟨 3. Cancellation with AbortController

Promises don’t support cancellation β€” but modern JS does.

const controller = new AbortController();

fetch(url, { signal: controller.signal });

// Later
controller.abort();

Essential for:

  • User navigation
  • Search-as-you-type
  • Cleanup on component unmount
  • Preventing stale responses

🟨 4. Timeouts for Async Operations

Never let async tasks run forever.

function withTimeout(promise, ms) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), ms);

  return promise.finally(() => clearTimeout(timeout));
}

Timeouts prevent:

  • Hanging UIs
  • Zombie requests
  • Resource leaks

🟨 5. Retry Patterns (But Don’t Be Naive)

❌ Bad retry logic

while (true) {
  await fetch(url);
}

βœ… Smart retries with backoff

async function retry(fn, retries = 3, delay = 500) {
  try {
    return await fn();
  } catch (err) {
    if (retries === 0) throw err;
    await new Promise(r => setTimeout(r, delay));
    return retry(fn, retries - 1, delay * 2);
  }
}

Used for:

  • Network instability
  • Rate-limited APIs
  • Temporary service failures

🟨 6. Async Iterators & Streaming Data

Async iterators let you consume data as it arrives.

for await (const chunk of stream) {
  process(chunk);
}

Used for:

  • File streaming
  • WebSockets
  • AI response streaming
  • Large datasets

They prevent loading everything into memory at once.


🟨 7. Queue-Based Async Processing

Sometimes async work must be serialized.

class AsyncQueue {
  constructor() {
    this.queue = Promise.resolve();
  }

  add(task) {
    this.queue = this.queue.then(task);
    return this.queue;
  }
}

Used for:

  • Writes to shared state
  • Logging systems
  • Rate-limited APIs

🟨 8. Worker Threads & Background Concurrency

Async β‰  parallel CPU work.

For CPU-heavy tasks, use:

  • Web Workers (browser)
  • Worker Threads (Node.js)
const worker = new Worker("worker.js");
worker.postMessage(data);

Critical for:

  • AI workloads
  • Image/video processing
  • Large data transforms

🟨 9. Async Anti-Patterns to Avoid

❌ Forgetting error handling
❌ Ignoring cancellation
❌ Overusing await in loops
❌ Infinite retries
❌ Blocking the event loop
❌ Unbounded concurrency

These cause flaky apps and β€œrandom” bugs.


🧩 Mini Exercises

1. Convert this to parallel execution:

for (const item of items) {
  await fetchData(item);
}

2. Add a timeout to this fetch:

fetch(url);

3. Why is this dangerous?

Promise.all(bigArray.map(doAsyncWork));

🟦 Async Best Practices Checklist

βœ” Know when to run tasks sequentially vs parallel
βœ” Always set timeouts
βœ” Use AbortController
βœ” Limit concurrency
βœ” Use retries with backoff
βœ” Stream data when possible
βœ” Offload CPU-heavy work
βœ” Never block the event loop


🏁 Final Thoughts

Promises and async/await are the foundation β€” not the ceiling β€” of modern JavaScript async programming.

Once you master advanced async patterns, you’ll build applications that are:

  • More resilient
  • More responsive
  • More scalable
  • Easier to debug
  • Safer under load

Next issue:
πŸ‘‰ Issue #6 β€” How JavaScript Engines Optimize Your Code (V8 Deep Dive)