Skip to content
JavaScript Basics: Produce Values One at a Time with Generators

JavaScript Basics: Produce Values One at a Time with Generators

October 17, 2026

A generator is a function that can pause and produce a sequence of values. A consumer asks for the next value when it needs one. This tutorial uses synchronous generators to explore pausing, iteration, and stopping early.

All examples use built-in features and were tested on Linux with Node.js v24.19.0. Strict assertions stop the program if a result is unexpected. No package installation is needed.

Prepare a scratch directory

You need Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. Keep using the same terminal. Save each named file as UTF-8; .mjs selects ES modules.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Step through a generator

Save this as step-through-generator.mjs:

import assert from 'node:assert/strict';

let started = false;
function* readingSteps() {
  started = true;
  yield 'Open the book';
  yield 'Read one page';
  return 'Finished';
}

const steps = readingSteps();
assert.equal(started, false);
console.log(`Started before next(): ${started}`);

const first = steps.next();
const second = steps.next();
const completed = steps.next();
const exhausted = steps.next();

assert.equal(started, true);
assert.deepEqual(first, { value: 'Open the book', done: false });
assert.deepEqual(second, { value: 'Read one page', done: false });
assert.deepEqual(completed, { value: 'Finished', done: true });
assert.deepEqual(exhausted, { value: undefined, done: true });

console.log(`First: ${first.value}, done: ${first.done}`);
console.log(`Second: ${second.value}, done: ${second.done}`);
console.log(`Return: ${completed.value}, done: ${completed.done}`);
console.log(`After completion: ${exhausted.value}, done: ${exhausted.done}`);

Run it:

node step-through-generator.mjs

Output:

Started before next(): false
First: Open the book, done: false
Second: Read one page, done: false
Return: Finished, done: true
After completion: undefined, done: true

Declare a generator with function*. Calling it creates a generator object; the body has not started yet. Each next() resumes execution until another yield or completion. Its result contains value and done.

yield pauses the body and gives a value to the consumer. return finishes it, with done: true. Further calls do not restart the generator. These calls run synchronously; yielding does not schedule background work.

2. Build and consume a small range

Save this as iterate-number-range.mjs:

import assert from 'node:assert/strict';

function* integerRange(start, end) {
  if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)) {
    throw new TypeError('Range endpoints must be safe integers');
  }
  for (let current = start; current < end; current += 1) {
    yield current;
  }
}

const range = integerRange(2, 5);
const numbers = [];
for (const number of range) {
  numbers.push(number);
}

assert.deepEqual(numbers, [2, 3, 4]);
assert.deepEqual([...range], []);
assert.deepEqual([...integerRange(5, 5)], []);
assert.deepEqual([...integerRange(5, 2)], []);
assert.throws(() => [...integerRange(1.5, 4)], TypeError);

console.log(`Numbers: ${numbers.join(', ')}`);
console.log(`Reusing exhausted object: ${JSON.stringify([...range])}`);
console.log(`Fresh generator: ${JSON.stringify([...integerRange(2, 5)])}`);

Run it:

node iterate-number-range.mjs

Output:

Numbers: 2, 3, 4
Reusing exhausted object: []
Fresh generator: [2,3,4]

for...of consumes yielded values automatically. This range includes start and excludes end; reversed or equal endpoints give no values. The validation rejects fractional and unsafe endpoints when iteration begins.

A generator object remembers its position. To iterate the sequence again, call the generator function again. Spread syntax also consumes a generator, but collects all its yielded values into an array. That removes the memory benefit of processing values one at a time, and spreading an endless sequence cannot finish. A final return value is not an item produced by for...of or spread.

3. Stop early and run cleanup

Save this as stop-generator-early.mjs:

import assert from 'node:assert/strict';

let cleanedUp = false;
function* numberedTickets() {
  try {
    yield 101;
    yield 102;
    yield 103;
  } finally {
    cleanedUp = true;
  }
}

const tickets = numberedTickets();
const selected = [];
for (const ticket of tickets) {
  selected.push(ticket);
  if (ticket === 102) break;
}

assert.deepEqual(selected, [101, 102]);
assert.equal(cleanedUp, true);
assert.deepEqual(tickets.next(), { value: undefined, done: true });
console.log(`Selected: ${selected.join(', ')}`);
console.log(`Cleanup ran: ${cleanedUp}`);
console.log(`Closed generator: ${tickets.next().done}`);

Run it:

node stop-generator-early.mjs

Output:

Selected: 101, 102
Cleanup ran: true
Closed generator: true

When break exits a for...of loop, the loop calls the iterator’s return() method if it has one. A generator has that method, so this exit reaches finally and closes this generator. The third ticket is never yielded.

Merely abandoning a generator after a manual next() call is different: it does not explicitly close it or guarantee prompt cleanup. If you manually drive a generator that needs cleanup, close it with return() when finished early. Keep cleanup straightforward; this example’s finally does not yield more values.

Clean up

In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:

rm -- step-through-generator.mjs iterate-number-range.mjs stop-generator-early.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on