ES6+ FeaturesFeatured

JavaScript Generators: Mastering Iterative Control Flow

Learn JavaScript generators for powerful iteration control. Master generator functions, yield expressions, and async generators for complex control flow patterns.

By JavaScript Document Team
generatorses6iteratorsasynccontrol-flow

Generators are special functions that can pause execution and resume later, allowing you to write iterative algorithms in a more elegant way. They provide powerful control over iteration and asynchronous flow.

Understanding Generators

Basic Generator Syntax

// Generator function declaration
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// Generator function expression
const genFunc = function* () {
  yield 'a';
  yield 'b';
};

// Using generators
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
for (const value of simpleGenerator()) {
  console.log(value); // 1, 2, 3
}

Generator Object Protocol

function* generatorProtocol() {
  console.log('Generator started');

  const value1 = yield 'First yield';
  console.log('Received:', value1);

  const value2 = yield 'Second yield';
  console.log('Received:', value2);

  return 'Generator done';
}

const gen = generatorProtocol();

console.log(gen.next()); // Generator started
// { value: 'First yield', done: false }

console.log(gen.next('Hello')); // Received: Hello
// { value: 'Second yield', done: false }

console.log(gen.next('World')); // Received: World
// { value: 'Generator done', done: true }

Yield Expressions

Basic Yield

function* yieldExamples() {
  // Simple yield
  yield 42;

  // Yield with expression
  yield 2 + 2;

  // Yield undefined
  yield;

  // Multiple yields in one line
  (yield 1, yield 2, yield 3); // Only the last yield is effective
}

const gen = yieldExamples();
console.log([...gen]); // [42, 4, undefined, 3]

Yield Delegation (yield*)

function* innerGenerator() {
  yield 'inner 1';
  yield 'inner 2';
}

function* outerGenerator() {
  yield 'outer 1';
  yield* innerGenerator(); // Delegate to another generator
  yield 'outer 2';
  yield* [1, 2, 3]; // Delegate to any iterable
}

const gen = outerGenerator();
console.log([...gen]);
// ['outer 1', 'inner 1', 'inner 2', 'outer 2', 1, 2, 3]

// Yield delegation with return values
function* genA() {
  yield 1;
  yield 2;
  return 'A done';
}

function* genB() {
  yield 'B start';
  const result = yield* genA();
  yield `Got result: ${result}`;
}

for (const value of genB()) {
  console.log(value);
}
// B start
// 1
// 2
// Got result: A done

Practical Generator Patterns

1. Infinite Sequences

// Fibonacci generator
function* fibonacci() {
  let [prev, curr] = [0, 1];

  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

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

const first10Fib = [...take(10, fibonacci())];
console.log(first10Fib);
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// ID generator
function* idGenerator(prefix = 'ID') {
  let id = 1;
  while (true) {
    yield `${prefix}-${id++}`;
  }
}

const userIds = idGenerator('USER');
console.log(userIds.next().value); // USER-1
console.log(userIds.next().value); // USER-2

2. Data Processing Pipelines

// Generator pipeline functions
function* map(fn, iterable) {
  for (const item of iterable) {
    yield fn(item);
  }
}

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

function* flatMap(fn, iterable) {
  for (const item of iterable) {
    yield* fn(item);
  }
}

// Usage
const numbers = [1, 2, 3, 4, 5];

const pipeline = map(
  (x) => x ** 2,
  filter((x) => x % 2 === 0, numbers)
);

console.log([...pipeline]); // [4, 16]

// Complex pipeline
function* complexPipeline(data) {
  yield* map(
    (x) => x * 2,
    filter(
      (x) => x > 2,
      flatMap((x) => [x, x + 0.5], data)
    )
  );
}

console.log([...complexPipeline([1, 2, 3, 4])]);
// [6, 7, 8, 9]

3. Tree Traversal

// Tree structure
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        { value: 4, children: [] },
        { value: 5, children: [] },
      ],
    },
    {
      value: 3,
      children: [{ value: 6, children: [] }],
    },
  ],
};

// Depth-first traversal
function* depthFirst(node) {
  yield node.value;
  for (const child of node.children) {
    yield* depthFirst(child);
  }
}

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

// Breadth-first traversal
function* breadthFirst(node) {
  const queue = [node];

  while (queue.length > 0) {
    const current = queue.shift();
    yield current.value;
    queue.push(...current.children);
  }
}

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

4. State Machines

function* trafficLight() {
  while (true) {
    // Green
    const greenTime = yield { color: 'green', duration: 30 };

    // Yellow
    const yellowTime = yield { color: 'yellow', duration: 5 };

    // Red
    const redTime = yield { color: 'red', duration: 25 };
  }
}

const light = trafficLight();

// Simulate traffic light
function runTrafficLight() {
  let current = light.next();

  const interval = setInterval(() => {
    console.log(`Light: ${current.value.color}`);
    current = light.next();
  }, 1000);

  // Stop after 10 seconds
  setTimeout(() => clearInterval(interval), 10000);
}

Error Handling in Generators

function* errorHandlingGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (error) {
    console.log('Caught error:', error.message);
    yield 'Error handled';
  } finally {
    console.log('Cleanup');
    yield 'Finally';
  }
}

const gen = errorHandlingGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }

// Throw error into generator
console.log(gen.throw(new Error('Something went wrong')));
// Caught error: Something went wrong
// { value: 'Error handled', done: false }

console.log(gen.next()); // Cleanup
// { value: 'Finally', done: false }

// Generator return method
function* returnExample() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('Generator terminated');
  }
}

const gen2 = returnExample();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.return('Stopped')); // Generator terminated
// { value: 'Stopped', done: true }

Async Generators

Basic Async Generators

// Async generator function
async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

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

// Async generator with delays
async function* delayedCounter() {
  let count = 0;
  while (count < 5) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    yield ++count;
  }
}

// Stream-like processing
async function* processStream(stream) {
  for await (const chunk of stream) {
    // Process chunk
    const processed = chunk.toUpperCase();
    yield processed;
  }
}

Real-World Async Generator Examples

// Paginated API requests
async function* paginatedFetch(baseUrl) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();

    yield* data.items;

    hasMore = data.hasNextPage;
    page++;
  }
}

// Usage
(async () => {
  const apiData = paginatedFetch('https://api.example.com/users');

  for await (const user of apiData) {
    console.log(user);
    // Process each user as they arrive
  }
})();

// File line reader
async function* readLines(file) {
  const text = await file.text();
  const lines = text.split('\n');

  for (const line of lines) {
    // Simulate async processing
    await new Promise((resolve) => setTimeout(resolve, 10));
    yield line.trim();
  }
}

// Rate-limited async generator
async function* rateLimited(items, delay) {
  for (const item of items) {
    yield item;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

Advanced Patterns

Generator Composition

// Compose multiple generators
function* compose(...generators) {
  for (const gen of generators) {
    yield* gen();
  }
}

function* gen1() {
  yield 1;
  yield 2;
}

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

const composed = compose(gen1, gen2);
console.log([...composed]); // [1, 2, 3, 4]

// Zip generators
function* zip(...iterables) {
  const iterators = iterables.map((it) => it[Symbol.iterator]());

  while (true) {
    const results = iterators.map((it) => it.next());

    if (results.some((r) => r.done)) {
      return;
    }

    yield results.map((r) => r.value);
  }
}

const zipped = zip([1, 2, 3], ['a', 'b', 'c'], [true, false, true]);
console.log([...zipped]); // [[1, 'a', true], [2, 'b', false], [3, 'c', true]]

Coroutines with Generators

// Simple coroutine scheduler
function* coroutine1() {
  console.log('Coroutine 1: Start');
  yield;
  console.log('Coroutine 1: Middle');
  yield;
  console.log('Coroutine 1: End');
}

function* coroutine2() {
  console.log('Coroutine 2: Start');
  yield;
  console.log('Coroutine 2: Middle');
  yield;
  console.log('Coroutine 2: End');
}

function scheduler(...coroutines) {
  const tasks = coroutines.map((co) => co());

  while (tasks.length > 0) {
    const task = tasks.shift();
    const { done } = task.next();

    if (!done) {
      tasks.push(task);
    }
  }
}

scheduler(coroutine1, coroutine2);
// Interleaved execution of coroutines

Generator-based State Management

function* createStore(initialState, reducer) {
  let state = initialState;

  while (true) {
    const action = yield state;

    if (action) {
      state = reducer(state, action);
    }
  }
}

// Usage
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore({ count: 0 }, reducer);

console.log(store.next().value); // { count: 0 }
console.log(store.next({ type: 'INCREMENT' }).value); // { count: 1 }
console.log(store.next({ type: 'INCREMENT' }).value); // { count: 2 }
console.log(store.next({ type: 'DECREMENT' }).value); // { count: 1 }

Performance Considerations

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

// Memory efficient - values generated on demand
const bigRange = range(0, 1000000);

// Only generates what's needed
function* take(n, iterable) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) return;
    yield item;
  }
}

const first5 = [...take(5, bigRange)];
console.log(first5); // [0, 1, 2, 3, 4]

Best Practices

  1. Use generators for lazy evaluation when dealing with large datasets
  2. Prefer async generators for asynchronous iteration
  3. Document generator behavior especially for complex flows
  4. Handle cleanup in finally blocks
  5. Consider memory usage when yielding large objects
  6. Use yield* for delegation to keep code clean

Conclusion

Generators provide a powerful way to control iteration and manage complex control flows in JavaScript. They enable lazy evaluation, simplify asynchronous code, and offer elegant solutions for problems involving sequences and state management. While they have a learning curve, mastering generators opens up new possibilities for writing efficient and maintainable code.