ESC

Type to search the knowledge base.

Generators

function* and yield — lazy sequences, custom iterators, two-way next(value), and how generators power async patterns.

advanced3 min read
  • javascript
  • generators
  • iterators
  • yield

A generator function (function*) returns a generator object — an iterator you can pause with yield and resume with .next(). They implement the iterable protocol for free and shine for lazy sequences, paginated streams, and custom iteration without allocating full arrays.

First contact

function* ids() {
  let i = 1;
  while (true) {
    yield i++;
  }
}

const gen = ids();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }

// for...of pulls until done (infinite here — don't without break)
function* range(n) {
  for (let i = 0; i < n; i++) yield i;
}
[...range(3)]; // [0, 1, 2]

Calling ids() does not run the body — it returns the generator. Execution starts on the first next().

Lazy pipelines

function* map(iter, fn) {
  for (const x of iter) yield fn(x);
}

function* filter(iter, pred) {
  for (const x of iter) if (pred(x)) yield x;
}

const result = [
  ...filter(
    map(range(10), (x) => x * 2),
    (x) => x % 3 === 0,
  ),
];

Nothing materializes until you consume with for...of, spread, or Array.from.

yield* and composition

function* a() {
  yield 1;
  yield 2;
}
function* b() {
  yield* a();
  yield 3;
}
[...b()]; // [1, 2, 3]

Two-way communication

next(value) sends a value into the generator as the result of the current yield:

function* talk() {
  const name = yield 'who?';
  yield `hi ${name}`;
}

const g = talk();
g.next(); // { value: 'who?', done: false }
g.next('Ada'); // { value: 'hi Ada', done: false }
g.next(); // { value: undefined, done: true }

gen.throw(err) injects an exception at the yield point. gen.return(v) terminates early and runs finally.

function* withCleanup() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log('cleanup');
  }
}
const h = withCleanup();
h.next();
h.return(); // logs cleanup

Infinite sequences (with care)

function* fibonacci() {
  let a = 0;
  let b = 1;
  for (;;) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function take(n, iter) {
  const out = [];
  for (const x of iter) {
    out.push(x);
    if (out.length === n) break;
  }
  return out;
}

take(6, fibonacci()); // [0,1,1,2,3,5]

Async angle

Generators are sync by themselves. Async generators (async function* + for await) handle async streams — see async iteration. Historically, libraries (co, Redux-Saga) used generators to express async flows before async/await was universal.

Interview answer

“function* returns an iterator that pauses at yield. next() resumes; next(value) feeds the last yield. Generators are iterable, support yield* delegation, and enable lazy sequences. finally runs on return/throw. For async streams I use async function*.”

Implementing iterables for UI data

function* walkDom(root) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    yield node;
    for (let i = node.childNodes.length - 1; i >= 0; i--) {
      stack.push(node.childNodes[i]);
    }
  }
}

for (const n of walkDom(document.body)) {
  if (n.nodeType === Node.TEXT_NODE && n.textContent.includes('TODO')) {
    console.log(n);
  }
}

Generators keep traversal state without building a giant array of nodes. Same idea for tree widgets and filesystem-like nested comments. Prefer async generators when each step awaits I/O.

Further reading

Related guides