π¦ 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)