Event Loop & Advanced Async Concurrency: Promise Combinators & Async Generators
Promise.all is the wrong default for parallel work. Promise.allSettled, Promise.any, and Promise.race are purpose-built combinators for resilient async composition, and async generators unlock non-blocking dataset streaming that async/await alone cannot express.
Event Loop & Advanced Async Concurrency: Promise Combinators & Async Generators
The runtime is not a black box — and neither is concurrent JavaScript. If you write Promise.all([fetchUser(), fetchOrders(), fetchInventory()]) and one endpoint goes down, the entire operation fails. Not slowly, not gracefully — it rejects immediately, discarding the two successful responses you already paid for. This is the wrong tool. This article builds the full mental model for async composition: which combinator to reach for when, and how async generators give you something async/await fundamentally cannot — a lazy, pull-based data stream that processes paginated APIs and large datasets without loading everything into memory.
This article assumes you understand the WHATWG Event Loop formal model: Macrotask → Microtask Checkpoint → Render. If you haven't read The JavaScript Event Loop: Macro → Micro → Render, read it first — it covers the scheduling architecture this article builds on. Part 3 starts at the composition layer.
1. The Event Loop Architecture — Composition Context
The call stack, microtask queue, and macrotask queue determine when your async callbacks execute. Promise resolution enqueues microtasks; setTimeout enqueues macrotasks. The full treatment is in the cross-referenced article. What matters for this article's scope:
- Promise resolution is always a microtask — resolved Promises schedule their
.thencallbacks in the microtask queue, which drains fully before the next macrotask or render frame queueMicrotask(fn)lets you enqueue your own microtasks explicitly — useful for deferring work until the current synchronous block finishes without waiting for a full macrotask cycle- The composition combinators (
allSettled,any,race) all build on the same microtask scheduling — their semantics differ in which resolutions they propagate
2. Promise Combinators — The Right Tool for the Job
Promise.all is the combinator most developers learn first and reach for exclusively. It has one severe limitation: it is fail-fast — if any input promise rejects, the combinator rejects immediately and discards all other pending or resolved results.
2.1 Promise.allSettled — Resilient Parallel Execution
Promise.allSettled waits for all promises to settle — fulfilled or rejected — and returns an array of outcome descriptors. No promise's result is ever discarded.
When to use: Any time you have multiple independent data sources and partial success is better than total failure. Dashboard aggregation, multi-source analytics, batch operations where per-item errors should not abort the batch.
2.2 Promise.any — First Success Wins
Promise.any resolves with the first fulfilled promise. It only rejects if all input promises reject — in which case it throws an AggregateError containing all the reasons.
When to use: Fallback chains, redundant data sources (read from any healthy replica), geographic CDN selection.
2.3 Promise.race — First Settled Wins (Including Rejections)
Promise.race settles with the first promise to settle — fulfilled or rejected. This distinction from Promise.any is critical for timeout patterns:
When to use: Timeout enforcement, detecting which of two operations is faster, speculative execution (run fast and slow paths, cancel the slower one).
2.4 The Decision Matrix

3. Async Generators and Iterators
async/await is a point-in-time abstraction: you await a single resolved value. But many real-world data sources are not single values — they are streams of values that arrive over time. Async generators are the language primitive for expressing this.
3.1 The Core Mechanic — Pull-Based Iteration
The yield keyword is the key: each yield suspends the generator and passes a value to the consumer. The generator does not advance to the next yield until the consumer explicitly asks for the next value. This is pull-based — the consumer controls the pace.
3.2 for await...of — Non-Blocking Large Dataset Iteration
An async generator is like a tap: it produces water (data) only when you open it (call .next()). Promise.all(pages) is filling a bathtub — it requests everything, holds it all, then lets you use it. for await...of on an async generator is drinking from the tap — you get one sip, process it, then ask for the next.
3.3 Processing Paginated APIs — Real Pattern

4. Combining Generators with Combinators
Async generators and Promise combinators solve different dimensions of the async problem:
| Problem | Tool | Why |
|---|---|---|
| Multiple independent parallel requests | Promise.allSettled |
Resilient multi-source aggregation |
| Fallback between data sources | Promise.any |
First success wins |
| Streaming sequential data | Async generator + for await |
Lazy, memory-constant iteration |
| Streaming from multiple sources concurrently | Async generator + Promise.race inside |
First available chunk from any source |
Summary
| Concept | Rule |
|---|---|
Promise.all |
All-or-nothing — use only when ALL results are required and one failure should abort |
Promise.allSettled |
Resilient parallel — correct default for independent parallel operations |
Promise.any |
First success — use for fallback chains and redundant sources |
Promise.race |
First settled — use for timeout enforcement and speculative execution |
| Async generator | Pull-based lazy sequence — produces one value per consumer .next() call |
for await...of |
Non-blocking iteration — processes each yielded value before requesting the next |
| Memory advantage | Async generator: O(1) per page · Promise.all(pages): O(n) for n pages |
What's Next
In Part 4, we move from runtime scheduling to code structure: Factory, Singleton, Observer, and Proxy patterns — not as academic exercises, but as engine-level primitives that explain how Vue 3 reactivity, Node.js
EventEmitter, and module singleton caches actually work. Understanding circular ESM dependency resolution turns a frustrating build error into a mechanical, solvable problem. Part 4 → Architectural Design Patterns
References
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.