Lazy Sequences with Generators: Compute Only What You Use
Most explanations of generators stop at "functions you can pause", which explains the mechanism and none of the point. The point is laziness: values are computed at the moment they're consumed, so sequences can be infinite, expensive steps run only if reached, and "give me the first 5 that match" does exactly that much work.
The infinite sequence that costs nothing
function* ids(prefix = 'id') {
let n = 0;
while (true) yield `${prefix}-${++n}`;
}
const nextId = ids('modal');
nextId.next().value; // "modal-1"
nextId.next().value; // "modal-2" - state lives between calls
An infinite while(true) that's completely safe, because iteration only advances on demand. This shape replaces the module-level counter variable and its reset bugs: the state is encapsulated in the generator instance, and different consumers get independent sequences.
Pipelines that stop early
function* filter(iter, fn) { for (const x of iter) if (fn(x)) yield x; }
function* map(iter, fn) { for (const x of iter) yield fn(x); }
function* take(iter, n) { for (const x of iter) { if (n-- <= 0) return; yield x; } }
// First 5 valid coupon codes from a huge candidate list:
const winners = [...take(filter(candidates(), isValid), 5)];
With array methods, filter() would validate every candidate before slice() threw the extra work away. The generator chain validates until it has 5 hits and stops - if isValid costs a millisecond and there are 100k candidates, that's the difference between instant and 100 seconds. This lazy-pipeline idea is the same one behind async generators for paginated APIs, minus the network.
Small delights
// Cycle through values forever - alternating row colors, round-robin assignment
function* cycle(items) {
while (true) yield* items;
}
const stripe = cycle(['even', 'odd']);
rows.forEach(row => row.classList.add(stripe.next().value));
// yield* delegates - compose sequences from sequences
function* allSettings() {
yield* defaults();
yield* userOverrides();
}
When an array is still the right answer
Generators are single-pass and forward-only: no .length, no random access, no iterating twice (the second pass finds it exhausted - the top gotcha in practice). If you need to count, sort or revisit, materialize with [...gen] and move on. My honest heuristic after years of use: arrays by default, generators the moment you catch yourself computing things a consumer might never look at - or building sequences with no natural end.
Support is ES6, i.e. universal for a decade. The feature isn't new; the habit of reaching for it is what's rare.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.