# JavaScript Generators and Iterators: How They Enable Lazy Evaluation

> Explore JavaScript generators and iterators to understand how they create sequences on demand and enable lazy evaluation, computing values only when needed. Learn this essential JS concept for efficient code.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-03

---

**JavaScript generators are special functions that pause execution at yield statements to produce sequences on demand, while iterators are objects implementing the next() method, together enabling lazy evaluation where values are computed only when requested rather than all at once.**

The `leonardomso/33-js-concepts` repository provides a comprehensive test suite demonstrating how **JavaScript generators and iterators** implement the iteration protocol for memory-efficient data processing. These patterns allow developers to work with infinite sequences and streaming data without loading entire datasets into memory.

## Understanding the Iterator Protocol

An **iterator** in JavaScript is any object that conforms to the iterator protocol by exposing a `next()` method. This method returns an object with two properties: `value`, containing the next item in the sequence, and `done`, a boolean indicating whether the iteration is complete. Any object implementing a `[Symbol.iterator]()` method that returns such an iterator becomes **iterable** and works with `for…of` loops, the spread operator, and `Array.from()`.

As demonstrated in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js) at lines 5-15, this protocol provides the standardized interface that enables systematic traversal of data structures while maintaining state between iterations.

## Generator Functions and the Yield Keyword

**Generators** are special functions declared with `function*` syntax that automatically create and return an iterator. Inside a generator, the `yield` keyword pauses execution and returns a value to the caller. When `.next()` is invoked again, execution resumes immediately after the last `yield`, preserving the function's local variable state.

This approach eliminates boilerplate code for manually constructing iterator objects. The test suite in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js) (lines 38-50) illustrates how generators handle the iteration protocol internally while providing a cleaner syntax for sequential value production.

## How Generators Enable Lazy Evaluation

The defining characteristic of generators is **lazy evaluation**—computation occurs strictly on demand when `.next()` or a consuming loop requests the next value. Unlike arrays where all elements are computed upfront, generators execute code only when needed. The test suite verifies this behavior by tracking a counter that remains at zero until the first `next()` call, then increments only upon subsequent requests (lines 78-94 in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js)).

This deferred execution model allows you to define infinite sequences or expensive calculations without performance penalties, as only consumed values are ever processed.

```javascript
function* lazyNumbers() {
  let i = 0;
  while (true) {
    i++;
    yield i; // Computed only when next() is called
  }
}

const gen = lazyNumbers();
console.log(gen.next().value); // → 1 (computation happens now)
console.log(gen.next().value); // → 2

```

## Practical Patterns with Lazy Evaluation

Generators enable architectural patterns that leverage deferred execution for efficient data workflows.

### Working with Infinite Sequences

Because generators compute values on demand, you can safely define infinite sequences without causing infinite loops or memory exhaustion. A generator produces values forever, while consumers control consumption using helper functions like `take()` that limit output to a specific count. This pattern appears in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js) at lines 102-118.

```javascript
function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

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

// Get only the first 5 numbers without infinite memory allocation
const result = [...take(5, naturalNumbers())];
console.log(result); // → [1, 2, 3, 4, 5]

```

### Maintaining Stateful Iteration

Generators naturally encapsulate mutable state across yields without exposing it externally. Each generator instance maintains its own scope, making them ideal for factories or counters that preserve state between invocations. The test file demonstrates this at lines 52-66 in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js).

```javascript
function* idFactory() {
  let id = 0;
  while (true) {
    yield ++id;
  }
}

const ids = idFactory();
console.log(ids.next().value); // → 1
console.log(ids.next().value); // → 2

```

### Building Data Processing Pipelines

You can chain generators to create efficient data processing pipelines where elements flow through `filter` and `map` operations only when requested. This eliminates intermediate arrays and reduces memory usage compared to traditional array methods. The pipeline pattern is shown at lines 86-99 in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js).

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

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

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

// Even numbers doubled, computed lazily without intermediate arrays
const pipeline = map(
  filter(range(1, 10), n => n % 2 === 0),
  n => n * 2
);
console.log([...pipeline]); // → [4, 8, 12, 16, 20]

```

## Asynchronous Generators and Streams

When declared as `async function*`, generators handle asynchronous data sources while maintaining lazy evaluation. Each `yield` can await a promise, and consumers use `for await…of` syntax to process data as it arrives. This pattern is essential for paginated API responses or streaming data without loading entire datasets into memory. The implementation is detailed in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js) at lines 70-84.

```javascript
async function* fetchPages(pages) {
  for (const page of pages) {
    const data = await Promise.resolve(page);
    yield data.items;
  }
}

const mockPages = [
  { items: ['a', 'b'] },
  { items: ['c', 'd'] },
  { items: ['e'] }
];

(async () => {
  const all = [];
  for await (const items of fetchPages(mockPages)) {
    all.push(...items);
  }
  console.log(all); // → ['a', 'b', 'c', 'd', 'e']
})();

```

## Summary

- **Iterators** are objects implementing the `next()` method returning `{ value, done }`, while **generators** are `function*` declarations that automatically create iterators using `yield`.
- **Lazy evaluation** ensures code runs only when `.next()` is called, allowing efficient handling of infinite sequences and expensive computations without upfront memory allocation.
- The test suite in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js) demonstrates patterns including stateful generators, pipelined data processing, and controlled consumption of infinite streams.
- **Asynchronous generators** extend lazy evaluation to promises, enabling memory-efficient processing of paginated APIs and real-time data streams.

## Frequently Asked Questions

### What is the difference between an iterator and a generator in JavaScript?

An **iterator** is any object implementing the iterator protocol with a `next()` method returning `{ value, done }`. A **generator** is a specific function type declared with `function*` that automatically creates an iterator, using `yield` to pause and resume execution. While you can manually construct iterator objects, generators handle the protocol implementation internally, providing a cleaner syntax for sequential value production.

### How does lazy evaluation improve memory usage in JavaScript applications?

Lazy evaluation improves memory usage by computing values on demand rather than storing entire collections in memory. When processing large datasets or infinite sequences, generators ensure only the current item exists in memory at any moment. This eliminates the need for intermediate arrays during `map` or `filter` operations, allowing pipelines to process extensive data with constant memory overhead.

### Can generators be used for asynchronous data streams?

Yes, **async generators** declared with `async function*` can yield promises and handle asynchronous data sources. Consumers use `for await…of` loops to process items as they resolve, making this pattern ideal for paginated API fetching, file streaming, or real-time data processing. According to the source code in [`tests/functions-execution/generators-iterators/generators-iterators.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/functions-execution/generators-iterators/generators-iterators.test.js), this maintains lazy evaluation even with asynchronous operations.

### When should I use a generator instead of a regular array?

Use **generators** when working with infinite sequences, streaming data, or when memory constraints prevent loading all items simultaneously. They are also ideal for maintaining internal iteration state or building composable data pipelines. Use regular arrays when you need random access to elements by index, when the dataset is small and finite, or when you need to iterate multiple times without regenerating values.