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...ofto consume asynchronous sequences. - Create async iterables via
Symbol.asyncIteratorandasync 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 awaitloop 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: