Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Lazy Sequences with Generators: Compute Only What You Use

C
Carlo Fontanos
· 1 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading