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.
- Generators are declared with function*.
- They return a generator object, not a final value immediately.
- They work with the iterator protocol, so they can be used in loops and custom iteration patterns.
- They are useful when you want values one at a time instead of all at once.
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:
- lazy evaluation, where values are computed on demand
- custom iterables that behave like built-in collections
- multi-step processes that need to pause and resume
- cleaner code for some stateful workflows, such as parsers or task runners
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); // 3Because 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
- Building custom iterables, such as a tree traversal or a range helper.
- Producing large sequences lazily instead of creating big arrays.
- Implementing stateful workflows, such as tokenizers or simple parsers.
- Breaking a task into staged steps where each step can pause and resume.
- Forwarding data from one iterable into another with yield*.
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
- A generator is not restartable. Once it finishes, you must call the generator function again to get a new iterator.
- yield can only be used inside a generator function, not in a normal function.
- Generators are synchronous. They do not automatically wait for promises or perform asynchronous iteration.
- If you break out of a for...of loop early, the generator is closed and cannot continue from that point.
- The first next() call cannot send a useful value into the generator because execution has not reached the first yield yet.
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
- Generators are functions declared with function* that can pause with yield.
- They return generator objects that support the iterator protocol.
- next() advances execution and returns an object with value and done.
- yield* delegates iteration to another iterable or generator.
- Generators are best for lazy values, custom iteration, and stateful workflows.
11. Practice Exercise
Build a generator that produces the first five odd numbers starting from 1.
- Create a generator function named oddNumbers.
- Use yield to produce five values.
- Consume the generator with for...of and print each number.
Expected output:
1
3
5
7
9Hint: 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.