JavaScript - Async Iterables
Overview
Estimated time: 30–40 minutes
Async iterables represent sequences whose values arrive over time. Learn how to consume them with for await...of
, create them with Symbol.asyncIterator
or async generator functions, and apply simple concurrency patterns.
Learning Objectives
- Use
for await...of
to consume asynchronous sequences. - Create async iterables via
Symbol.asyncIterator
andasync function*
. - Bridge promises to async iteration and control cancellation.
- Apply basic concurrency with mapping and limits.
Prerequisites
- JavaScript - Iterables & Generators
- JavaScript - JSON (recommended)
Consuming async iterables
Use for await...of
inside an async
function to consume an async iterable.
async function* countAsync(n, delayMs = 100) {
for (let i = 1; i <= n; i++) {
await new Promise(r => setTimeout(r, delayMs));
yield i;
}
}
(async () => {
for await (const x of countAsync(3)) {
console.log(x); // 1, then 2, then 3 over time
}
})();
Creating async iterables
// Async generator (most convenient)
async function* fromArrayDelayed(arr, delayMs = 50) {
for (const x of arr) { await new Promise(r => setTimeout(r, delayMs)); yield x; }
}
// Custom object implementing Symbol.asyncIterator
const ticker = (ticks = 3, delayMs = 100) => ({
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i >= ticks) return { done: true };
await new Promise(r => setTimeout(r, delayMs));
return { value: ++i, done: false };
},
async return() { return { done: true }; } // handle early cancellation
};
}
});
Bridging promises
// for await...of can iterate a sync iterable of promises, awaiting each element
const promises = [1,2,3].map(n => new Promise(r => setTimeout(() => r(n*n), n*30)));
(async () => {
for await (const v of promises) {
console.log('square:', v);
}
})();
Concurrency patterns
// Simple concurrent map with a limit (preserves input order)
async function* mapAsync(iterable, fn, concurrency = 2) {
const it = iterable[Symbol.iterator]();
let index = 0;
const inFlight = new Map();
function launch() {
const next = it.next();
if (next.done) return;
const i = index++;
inFlight.set(i, Promise.resolve(fn(next.value, i)).then(v => ({ i, v })));
}
// Prime the pool
for (let k = 0; k < concurrency; k++) launch();
while (inFlight.size) {
const { i, v } = await Promise.race(inFlight.values());
inFlight.delete(i);
yield v; // yields as tasks complete (not strictly in input order)
launch();
}
}
Common Pitfalls
- Top-level await: Not available in all environments; wrap in an async IIFE.
- Backpressure: Generators that outpace consumers can buffer; design with limits or pauses.
- Cancellation: Breaking a
for await
loop callsreturn()
on the iterator if implemented. - Error propagation: Exceptions inside async generators reject the next
next()
call; wrap consumers intry/catch
.
Try it
Run to see async iteration, bridging promises, and limited concurrency: