Skip to main content

Generators and Iterators in JavaScript

Created: May 8, 2026 Larry Qu 8 min read

Generators and iterators are powerful features for creating lazy, efficient code. This article covers understanding iteration protocols, creating generators, and practical applications.

Introduction

Generators and iterators enable:

  • Lazy evaluation of sequences
  • Memory-efficient processing
  • Simplified state management
  • Elegant async patterns
  • Custom iteration logic

Understanding generators helps you:

  • Write more efficient code
  • Handle large datasets
  • Implement complex workflows
  • Build powerful abstractions

Iteration Protocol

Iterable Protocol

// An object is iterable if it has a Symbol.iterator method
// that returns an iterator

const iterable = {
  [Symbol.iterator]() {
    return {
      next() {
        // Return { value, done }
      }
    };
  }
};

// Can be used in for...of loops
for (const value of iterable) {
  console.log(value);
}

Iterator Protocol

// An iterator has a next() method that returns:
// { value: any, done: boolean }

const iterator = {
  next() {
    return { value: 1, done: false };
  }
};

// Call next() to get values
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 1, done: false }

Creating Custom Iterables

// ✅ Good: Create a custom iterable
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

// Usage
const range = new Range(1, 5);
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}
// Practical example: Iterable for object entries
class ObjectEntries {
  constructor(obj) {
    this.obj = obj;
  }

  [Symbol.iterator]() {
    const keys = Object.keys(this.obj);
    let index = 0;

    return {
      next: () => {
        if (index < keys.length) {
          const key = keys[index++];
          return {
            value: [key, this.obj[key]],
            done: false
          };
        }
        return { done: true };
      }
    };
  }
}

// Usage
const obj = { a: 1, b: 2, c: 3 };
const entries = new ObjectEntries(obj);

for (const [key, value] of entries) {
  console.log(`${key}: ${value}`);
}

Generator Functions

Basic Generators

// Generator function: uses function* and yield
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// Calling generator returns an iterator
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 are iterable
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

// Can be used in for...of
for (const num of countTo(5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Can be spread
const numbers = [...countTo(5)];
console.log(numbers); // [1, 2, 3, 4, 5]

Generator State

// Generators maintain state between yields
function* counter() {
  let count = 0;
  while (true) {
    yield count++;
  }
}

const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

Sending Values to Generators

// Send values back to generator with next(value)
function* echo() {
  const value1 = yield 'first';
  console.log('Received:', value1);
  
  const value2 = yield 'second';
  console.log('Received:', value2);
}

const gen = echo();

console.log(gen.next());        // { value: 'first', done: false }
console.log(gen.next('hello')); // Logs: Received: hello
                                // { value: 'second', done: false }
console.log(gen.next('world')); // Logs: Received: world
                                // { value: undefined, done: true }
// Practical example: Generator with input
function* fibonacci(limit) {
  let a = 0, b = 1;
  
  while (a < limit) {
    const next = yield a;
    
    // If value sent, use it; otherwise continue sequence
    if (next !== undefined) {
      a = next;
    } else {
      [a, b] = [b, a + b];
    }
  }
}

const gen = fibonacci(100);
console.log(gen.next().value);      // 0
console.log(gen.next().value);      // 1
console.log(gen.next().value);      // 1
console.log(gen.next(50).value);    // 50 (sent value)
console.log(gen.next().value);      // 51

Practical Generator Patterns

Lazy Sequences

// ✅ Good: Lazy evaluation with generators
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

function* map(iterable, fn) {
  for (const value of iterable) {
    yield fn(value);
  }
}

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

// Lazy evaluation: only computes what's needed
const result = filter(
  map(range(1, 1000000), x => x * 2),
  x => x % 3 === 0
);

// Only processes first 5 values
for (const value of result) {
  console.log(value);
  if (value > 30) break;
}

Reading Files Line by Line

// ✅ Good: Read large files efficiently
async function* readLines(filePath) {
  const fs = require('fs');
  const readline = require('readline');
  
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

// Usage: Process large file without loading into memory
async function processFile(filePath) {
  for await (const line of readLines(filePath)) {
    console.log(line);
  }
}

Infinite Sequences

// ✅ Good: Generate infinite sequences
function* infiniteSequence(start = 0, step = 1) {
  let current = start;
  while (true) {
    yield current;
    current += step;
  }
}

// Take first N values
function* take(iterable, n) {
  let count = 0;
  for (const value of iterable) {
    if (count++ >= n) break;
    yield value;
  }
}

// Usage
const evens = take(infiniteSequence(0, 2), 5);
console.log([...evens]); // [0, 2, 4, 6, 8]

Delegating to Other Generators

// ✅ Good: Use yield* to delegate
function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield 3;
  yield 4;
}

function* combined() {
  yield* generator1();
  yield* generator2();
}

console.log([...combined()]); // [1, 2, 3, 4]
// Practical example: Tree traversal
function* traverse(node) {
  yield node.value;
  
  if (node.left) {
    yield* traverse(node.left);
  }
  
  if (node.right) {
    yield* traverse(node.right);
  }
}

// Usage
const tree = {
  value: 1,
  left: { value: 2, left: null, right: null },
  right: { value: 3, left: null, right: null }
};

for (const value of traverse(tree)) {
  console.log(value); // 1, 2, 3
}

Generator Methods

return() Method

// return() terminates the generator
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

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

throw() Method

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

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

Advanced Patterns

Generator-Based Coroutines

// ✅ Good: Use generators for async-like control flow
function* fetchData() {
  try {
    const response = yield fetch('/api/data');
    const data = yield response.json();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

// Runner function to execute generator
function run(generator) {
  const iterator = generator();
  
  function handle(result) {
    if (result.done) return;
    
    Promise.resolve(result.value)
      .then(res => handle(iterator.next(res)))
      .catch(err => iterator.throw(err));
  }
  
  handle(iterator.next());
}

// Usage
run(fetchData);

State Machine with Generators

// ✅ Good: Implement state machine
function* trafficLight() {
  while (true) {
    yield 'red';
    yield 'yellow';
    yield 'green';
  }
}

const light = trafficLight();
console.log(light.next().value);  // red
console.log(light.next().value);  // yellow
console.log(light.next().value);  // green
console.log(light.next().value);  // red

Memoization with Generators

// ✅ Good: Cache results with generators
function* memoize(fn) {
  const cache = new Map();
  
  while (true) {
    const arg = yield;
    
    if (cache.has(arg)) {
      console.log('From cache:', arg);
    } else {
      const result = fn(arg);
      cache.set(arg, result);
      console.log('Computed:', arg);
    }
  }
}

const memo = memoize(x => x * 2);
memo.next();           // Start generator
memo.next(5);          // Logs: Computed: 5
memo.next(5);          // Logs: From cache: 5

Best Practices

  1. Use generators for lazy evaluation:
    // ✅ Good
    function* lazyRange(n) {
      for (let i = 0; i < n; i++) {
        yield i;
      }
    }
    ```javascript
    
  2. Delegate with yield:*
    // ✅ Good
    function* combined() {
      yield* generator1();
      yield* generator2();
    }
    ```javascript
    
  3. Handle errors properly:
    // ✅ Good
    function* gen() {
      try {
        yield value;
      } catch (error) {
        // Handle error
      }
    }
    ```javascript
    
  4. Use for large datasets:
    // ✅ Good - memory efficient
    function* readLargeFile(path) {
      // Yield one item at a time
    }
    ```javascript
    

Common Mistakes

  1. Forgetting function syntax:*
    // ❌ Bad
    function gen() {
      yield 1;
    }
    
    // ✅ Good
    function* gen() {
      yield 1;
    }
    ```javascript
    
  2. Not understanding lazy evaluation:
    // ❌ Bad - evaluates all values
    const values = [1, 2, 3, 4, 5].map(x => x * 2);
    
    // ✅ Good - lazy evaluation
    function* values() {
      for (let i = 1; i <= 5; i++) {
        yield i * 2;
      }
    }
    ```javascript
    
  3. Forgetting generators are iterators:
    // ❌ Bad
    const gen = generator();
    gen.next();
    gen.next();
    
    // ✅ Good - use for...of
    for (const value of generator()) {
      console.log(value);
    }
    

Summary

Generators and iterators are powerful tools. Key takeaways:

  • Iterables have Symbol.iterator method
  • Iterators have next() method
  • Generators use function* and yield
  • Generators are lazy and memory-efficient
  • Use yield* to delegate to other generators
  • Generators can receive values via next(value)
  • Perfect for large datasets and sequences

Next Steps

Resources

Comments

Share this article

Scan to read on mobile