JavaScript Generators (function*) Explained with Examples

JavaScript generators let you pause a function and continue it later, which makes them useful for custom iteration, lazy values, and step-by-step control flow. They are a core part of the iteration model in modern JavaScript.

Quick answer: A generator is a function declared with function* that can pause with yield and resume later with next(). It returns a generator object that follows the iterator protocol.

Difficulty: Intermediate

You'll understand this better if you know: how functions work in JavaScript, what arrays and loops do, and the basics of objects and return values.

1. What Are JavaScript Generators?

A generator is a special kind of function that does not run from top to bottom in one pass. Instead, it can stop at a yield statement and continue later from the same point.

Think of a generator as a paused function with memory of where it stopped and what local variables it already has.

2. Why JavaScript Generators Matter

Generators matter because they give you fine-grained control over iteration and computation. Instead of building a full array first, you can produce values only when they are needed.

This helps with:

They are not required for everyday looping, but they are very valuable when you need custom iteration behavior or want to avoid doing all work up front.

3. Basic Syntax or Core Idea

A generator is written with function*, and it uses yield to produce values one at a time.

Minimal generator syntax

The following example shows the simplest possible generator.

function* countToThree() {
  yield 1;
  yield 2;
  yield 3;
}

const numbers = countToThree();

console.log(numbers.next()); // { value: 1, done: false }
console.log(numbers.next()); // { value: 2, done: false }
console.log(numbers.next()); // { value: 3, done: false }
console.log(numbers.next()); // { value: undefined, done: true }

Each call to next() resumes the function until the next yield or until the function finishes.

How yield works

The yield keyword both produces a value and pauses execution. When the generator resumes, it continues from the next line after that yield.

function* demo() {
  console.log("before");
  yield "paused";
  console.log("after");
}

const iter = demo();
iter.next(); // logs "before"
iter.next(); // logs "after"

This shows that a generator preserves local state between calls.

4. Step-by-Step Examples

Example 1: Iterating through names

Here is a generator that yields a few strings in sequence. This is a simple way to see the iteration pattern.

function* names() {
  yield "Ada";
  yield "Grace";
  yield "Linus";
}

for (const name of names()) {
  console.log(name);
}

The generator can be used directly in for...of because it is iterable.

Example 2: Passing values back into a generator

A generator can receive values through next(value). The first call starts the function, and later calls can send data back in.

function* askName() {
  const name = yield "What is your name?";
  return `Hello, ${name}!`;
}

const greeting = askName();
console.log(greeting.next().value); // "What is your name?"
console.log(greeting.next("Ava").value); // "Hello, Ava!"

This example shows that generators can pause, ask for input, and then continue.

Example 3: Infinite sequence with lazy output

Generators are especially useful for sequences that do not need to be fully stored in memory.

function* naturalNumbers() {
  let n = 1;

  while (true) {
    yield n;
    n = n + 1;
  }
}

const nums = naturalNumbers();
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
console.log(nums.next().value); // 3

Because values are produced only when requested, this pattern avoids allocating a huge array up front.

Example 4: Delegating to another generator with yield*

The yield* syntax forwards iteration to another iterable or generator.

function* partOne() {
  yield "A";
  yield "B";
}

function* partTwo() {
  yield "C";
}

function* allLetters() {
  yield* partOne();
  yield* partTwo();
}

console.log([...allLetters()]); // ["A", "B", "C"]

This is useful when one generator is built from smaller pieces.

5. Practical Use Cases

Generators are usually a design choice for controlled iteration, not a replacement for every array method or loop.

6. Common Mistakes

Mistake 1: Forgetting to call the generator

A generator function returns a generator object only after you call it. If you try to iterate over the function itself, the code will not behave as expected.

Problem: The function reference is not iterable, so the loop fails or produces the wrong result.

function* letters() {
  yield "x";
  yield "y";
}

for (const letter of letters) {
  console.log(letter);
}

Fix: Call the generator function so you get the iterable generator object.

function* letters() {
  yield "x";
  yield "y";
}

for (const letter of letters()) {
  console.log(letter);
}

The corrected version works because letters() returns an iterable generator object.

Mistake 2: Using return when you meant yield

return ends the generator immediately. If you want multiple values, use yield for each value you want to produce.

Problem: The generator stops after the first return, so later values are never produced.

function* colors() {
  return "red";
  yield "green";
  yield "blue";
}

Fix: Use yield for values that should be produced during iteration, and reserve return for the final completion value if needed.

function* colors() {
  yield "red";
  yield "green";
  yield "blue";
}

The fixed version produces each color in sequence as intended.

Mistake 3: Expecting next() to return the yielded value directly

The next() method returns an object with both value and done. New developers often log the whole object or try to use it as if it were the raw value.

Problem: This code treats the result object like a plain string, which leads to confusing output or logic bugs.

function* numbers() {
  yield 10;
}

const value = numbers().next();
console.log(value + 1);

Fix: Read the value property from the iterator result.

function* numbers() {
  yield 10;
}

const result = numbers().next();
console.log(result.value + 1);

The corrected version works because you are using the actual yielded value, not the wrapper object.

7. Best Practices

Use generators when values are naturally produced one at a time

Generators are a good fit for sequences, traversals, and on-demand computation. They are less helpful when you already have all data available and just need a simple array.

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

This keeps the code readable and avoids unnecessary arrays when you only need a stream of values.

Keep generator state small and meaningful

A generator stores its local variables between pauses, so it is best to keep only the state you actually need.

function* pager(pages) {
  let index = 0;

  while (index < pages.length) {
    yield pages[index++];
  }
}

That makes the generator easier to understand and reduces accidental complexity.

Prefer for...of when you only need iteration

If you do not need to control each next() call manually, use for...of for simpler code.

function* ids() {
  yield 101;
  yield 102;
}

for (const id of ids()) {
  console.log(id);
}

This is usually clearer than manually calling next() several times.

8. Limitations and Edge Cases

Tip: If you need asynchronous iteration, look at async generators and for await...of instead of regular generators.

9. Practical Mini Project

Let’s build a small range utility that uses a generator to produce numbers lazily and then consumes them in different ways.

function* range(start, end, step = 1) {
  if (step <= 0) {
    throw new Error("step must be greater than 0");
  }

  for (let n = start; n <= end; n += step) {
    yield n;
  }
}

const values = [...range(2, 10, 2)];
console.log(values); // [2, 4, 6, 8, 10]

for (const value of range(1, 5)) {
  console.log(value);
}

This mini project shows how a generator can behave like a reusable sequence builder. The same range logic can be consumed as an array or directly in a loop.

10. Key Points

11. Practice Exercise

Build a generator that produces the first five odd numbers starting from 1.

Expected output:

1
3
5
7
9

Hint: Start at 1 and add 2 after each yield.

Solution:

function* oddNumbers() {
  let current = 1;

  for (let count = 0; count < 5; count++) {
    yield current;
    current += 2;
  }
}

for (const value of oddNumbers()) {
  console.log(value);
}

12. Final Summary

JavaScript generators give you a way to pause and resume a function, making them ideal for custom iteration and lazy value production. The combination of function*, yield, next(), and yield* forms a flexible toolset for advanced iteration patterns.

For beginners, the most important idea is that a generator is not a regular function call. It creates an iterator that you drive step by step. Once that mental model clicks, generators become much easier to use for ranges, traversals, and stateful workflows.

If you want to continue learning, the next natural topic is the iterator protocol and how generators relate to for...of, arrays, and async generators.