Skip to main content

Generators in JavaScript

Generators are special functions that can pause execution and resume later, allowing you to produce a sequence of values over time rather than computing them all at once.

Basic Generator Syntax

Generators are defined using the function* syntax and use the yield keyword to pause execution.
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generators return an iterator object. Each call to next() resumes execution until the next yield statement.

Using Generators with for…of

Generators are iterable, so they work with for...of loops:
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

for (const num of countTo(5)) {
  console.log(num);
}
// 1
// 2
// 3
// 4
// 5

Infinite Sequences

Generators can produce infinite sequences because values are computed lazily:
function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const gen = infiniteSequence();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// Can continue forever

Fibonacci Generator

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
console.log(fib.next().value); // 8

Passing Values to Generators

You can pass values to a generator using next(value):
function* generatorWithInput() {
  const input1 = yield 'Give me input 1';
  console.log('Received:', input1);
  
  const input2 = yield 'Give me input 2';
  console.log('Received:', input2);
  
  return 'Done!';
}

const gen = generatorWithInput();
console.log(gen.next());        // { value: 'Give me input 1', done: false }
console.log(gen.next('First')); // Received: First
                                 // { value: 'Give me input 2', done: false }
console.log(gen.next('Second'));// Received: Second
                                 // { value: 'Done!', done: true }
The first next() call starts the generator but doesn’t pass a value. Subsequent next(value) calls pass the value to the last yield.

yield* (Delegating to Another Generator)

You can delegate to another generator using yield*:
function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield 'a';
  yield* generator1();
  yield 'b';
}

for (const value of generator2()) {
  console.log(value);
}
// 'a'
// 1
// 2
// 'b'

Practical Examples

ID Generator

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const ids = idGenerator();
const user1 = { id: ids.next().value, name: 'Alice' };
const user2 = { id: ids.next().value, name: 'Bob' };
const user3 = { id: ids.next().value, name: 'Charlie' };

Range Generator

function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

const numbers = [...range(1, 10, 2)];
console.log(numbers); // [1, 3, 5, 7, 9]

Batch Processing

function* batch(array, size) {
  for (let i = 0; i < array.length; i += size) {
    yield array.slice(i, i + size);
  }
}

const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (const chunk of batch(data, 3)) {
  console.log(chunk);
}
// [1, 2, 3]
// [4, 5, 6]
// [7, 8, 9]
// [10]

Tree Traversal

class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }

  *traverse() {
    yield this.value;
    for (const child of this.children) {
      yield* child.traverse();
    }
  }
}

const tree = new TreeNode(1, [
  new TreeNode(2, [
    new TreeNode(4),
    new TreeNode(5)
  ]),
  new TreeNode(3, [
    new TreeNode(6),
    new TreeNode(7)
  ])
]);

console.log([...tree.traverse()]); // [1, 2, 4, 5, 3, 6, 7]

Async Data Fetching

function* fetchPages(urls) {
  for (const url of urls) {
    yield fetch(url).then(r => r.json());
  }
}

const urls = [
  'https://api.example.com/page1',
  'https://api.example.com/page2',
  'https://api.example.com/page3'
];

const pages = fetchPages(urls);

// Process one at a time
for (const pagePromise of pages) {
  const data = await pagePromise;
  console.log(data);
}

Generator Methods

return()

Force a generator to complete:
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();
console.log(g.next());        // { value: 1, done: false }
console.log(g.return('end')); // { value: 'end', done: true }
console.log(g.next());        // { value: undefined, done: true }

throw()

Throw an error into the generator:
function* gen() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (e) {
    console.log('Caught:', e.message);
  }
}

const g = gen();
console.log(g.next());                    // { value: 1, done: false }
console.log(g.throw(new Error('Error'))); // Caught: Error
                                           // { value: undefined, done: true }

Lazy Evaluation

Generators enable lazy evaluation - values are computed only when needed:
function* map(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}

function* filter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

function* take(iterable, n) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) return;
    yield item;
  }
}

// Create a pipeline
const numbers = function* () {
  let i = 0;
  while (true) yield i++;
}();

const doubled = map(numbers, x => x * 2);
const evens = filter(doubled, x => x % 4 === 0);
const firstTen = take(evens, 10);

console.log([...firstTen]); // [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]
This pipeline processes only the values needed, not all infinite numbers. Each operation is performed lazily.

Async Generators

Async generators combine generators with async/await:
async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

(async () => {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
})();
// 1
// 2
// 3

Async Data Stream

async function* fetchUserPages(userId, maxPages = 5) {
  let page = 1;
  while (page <= maxPages) {
    const response = await fetch(`/api/users/${userId}/posts?page=${page}`);
    const data = await response.json();
    if (data.posts.length === 0) break;
    yield data.posts;
    page++;
  }
}

// Usage
(async () => {
  for await (const posts of fetchUserPages(123)) {
    console.log('Page of posts:', posts);
  }
})();

Use Cases

Generate infinite sequences like IDs, random numbers, or mathematical sequences without consuming memory.
Process large datasets or streams one item at a time without loading everything into memory.
Create custom iterators for complex data structures like trees, graphs, or custom collections.
Implement state machines where each yield represents a state transition.
Coordinate multiple async operations with async generators, useful for pagination or streaming.

Best Practices

When working with large or infinite sequences, generators save memory by computing values on demand.
Generators are perfect for creating custom iterators for complex data structures.
Use generator pipelines to process data lazily, computing only what’s needed.
Use try/catch blocks inside generators to handle errors during iteration.

Build docs developers (and LLMs) love