Learn JavaScript generators and iterators. Understand yield, lazy evaluation, infinite sequences, and async generators.
What if a function could pause mid-execution, return a value, and then resume right where it left off? What if you could create a sequence of values that are computed only when you ask for them — not all at once?
This is the power of generators. Introduced in the ECMAScript 2015 specification, these are functions that can pause with yield and pick up where they left off. Combined with iterators (objects that define how to step through a sequence), they open up patterns like lazy evaluation, infinite sequences, and clean data pipelines.
What you’ll learn in this guide:
What iterators are and how the iteration protocol works
Generator functions with function* and yield (they’re lazier than you think)
The difference between yield and return (it trips people up!)
How to make any object iterable with Symbol.iterator
Lazy evaluation — why generators are so memory-efficient
Practical patterns: pagination, ID generation, state machines
Async generators and for await...of for streaming data
Prerequisites: This guide assumes you’re comfortable with closures and higher-order functions. If those concepts are new to you, read those guides first!
Before getting into generators, we need to cover iterators, the foundation that makes generators work.An iterator is an object that defines a sequence and provides a way to access values one at a time. It must have a .next() method that returns an object with two properties:
value — the next value in the sequence
done — true if the sequence is finished, false otherwise
Lazy evaluation — Values are computed only when you ask for them, not upfront
Memory efficiency — You don’t need to hold the entire sequence in memory
Say you need to process a million records. With an array, you’d load all million into memory. With an iterator, you process one at a time. Memory stays flat.
for...of uses iterators under the hood. When you write for (const item of array), JavaScript is actually calling the iterator’s .next() method repeatedly until done is true. According to the ECMAScript specification, any object that implements the Symbol.iterator method is considered iterable and can be used with for...of, spread syntax, and destructuring.
A generator is a function that can stop mid-execution, hand you a value, and pick up where it left off later. You create one using function* (note the asterisk) and pause it with the yield keyword.
Copy
Ask AI
// The asterisk (*) makes this a generator functionfunction* myGenerator() { console.log('Starting...') yield 'First value' console.log('Resuming...') yield 'Second value' console.log('Finishing...') return 'Done!'}
When you call a generator function, the code inside doesn’t run yet. You just get back a generator object (which is an iterator):
yield is what makes generators tick. It pauses the function and sends a value back to the caller. When you call .next() again, execution picks up right after the yield.
Both yield and return can return values, but they behave very differently:
yield
return
Pauses the generator
Ends the generator
done: false
done: true
Can have multiple
Only one matters
Value accessible in for...of
Value NOT accessible in for...of
Copy
Ask AI
function* example() { yield 'A' // Pauses, done: false yield 'B' // Pauses, done: false return 'C' // Ends, done: true}// With for...of — return value is ignored!for (const val of example()) { console.log(val)}// Output: A, B (no C!)// With .next() — you can see the return valueconst gen = example()console.log(gen.next()) // { value: 'A', done: false }console.log(gen.next()) // { value: 'B', done: false }console.log(gen.next()) // { value: 'C', done: true }
Common gotcha: The value from return is not included when iterating with for...of, spread syntax, or Array.from(). Use yield for all values you want to iterate over.
You can also send values into a generator by passing them to .next(value). The value becomes the result of the yield expression inside the generator:
Copy
Ask AI
function* conversation() { const name = yield 'What is your name?' const color = yield `Hello, ${name}! What's your favorite color?` yield `${color} is a great color, ${name}!`}const chat = conversation()// First .next() — no value needed, just starts the generatorconsole.log(chat.next().value)// "What is your name?"// Second .next() — pass in the answerconsole.log(chat.next('Alice').value)// "Hello, Alice! What's your favorite color?"// Third .next() — pass in another answerconsole.log(chat.next('Blue').value)// "Blue is a great color, Alice!"
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ DATA FLOW WITH yield │├─────────────────────────────────────────────────────────────────────────┤│ ││ CALLER GENERATOR ││ ││ .next() ─────────────────────► starts execution ││ ◄───────────────────── yield 'question' ││ ││ .next('Alice') ─────────────────────► const name = 'Alice' ││ ◄───────────────────── yield 'Hello Alice' ││ ││ .next('Blue') ─────────────────────► const color = 'Blue' ││ ◄───────────────────── yield 'Blue is great' ││ ││ The value passed to .next() becomes the RESULT of the yield ││ expression inside the generator. ││ │└─────────────────────────────────────────────────────────────────────────┘
Why no value in the first .next()? The first call starts the generator and runs until the first yield. There’s no yield waiting to receive a value yet, so anything you pass gets ignored.
If there’s no try/catch, the error propagates out:
Copy
Ask AI
function* fragileGenerator() { yield 'A' yield 'B' // Error thrown here if we call .throw() after first yield}const gen = fragileGenerator()gen.next() // { value: 'A', done: false }try { gen.throw(new Error('Boom!'))} catch (e) { console.log(e.message) // "Boom!"}
These methods complete the generator’s interface. While .next() is used most often, .return() and .throw() give you full control over generator execution — useful for resource cleanup and error handling in complex workflows.
Here’s a Range class you can loop over with for...of:
Copy
Ask AI
class Range { constructor(start, end, step = 1) { this.start = start this.end = end this.step = step } // Generator makes this easy! *[Symbol.iterator]() { for (let i = this.start; i <= this.end; i += this.step) { yield i } }}const oneToFive = new Range(1, 5)console.log([...oneToFive]) // [1, 2, 3, 4, 5]const evens = new Range(0, 10, 2)console.log([...evens]) // [0, 2, 4, 6, 8, 10]// Works with for...offor (const n of new Range(1, 3)) { console.log(n) // 1, 2, 3}
When you write a for...of loop, JavaScript does this behind the scenes:
1
Get the iterator
JavaScript calls iterable[Symbol.iterator]() to get an iterator object.
2
Call .next()
The loop calls iterator.next() to get the first { value, done } result.
3
Check if done
If done is false, the value goes into your loop variable.
4
Repeat until done
Steps 2-3 repeat until done is true, then the loop exits.
Here’s what that looks like in code:
Copy
Ask AI
// This:for (const item of iterable) { console.log(item)}// Is equivalent to this:const iterator = iterable[Symbol.iterator]()let result = iterator.next()while (!result.done) { const item = result.value console.log(item) result = iterator.next()}
When to make something iterable: If your object represents a collection or sequence of values, making it iterable allows it to work with for...of, spread syntax, Array.from(), destructuring, and more.
Compare these two approaches for creating a range of numbers:
Copy
Ask AI
// Eager evaluation — creates entire array in memoryfunction rangeArray(start, end) { const result = [] for (let i = start; i <= end; i++) { result.push(i) } return result}// Lazy evaluation — computes values on demandfunction* rangeGenerator(start, end) { for (let i = start; i <= end; i++) { yield i }}// For small ranges, both work fineconsole.log(rangeArray(1, 5)) // [1, 2, 3, 4, 5]console.log([...rangeGenerator(1, 5)]) // [1, 2, 3, 4, 5]// For large ranges, generators shine// rangeArray(1, 1000000) — Creates array of 1 million numbers!// rangeGenerator(1, 1000000) — Creates nothing until you iterate
Because generators are lazy, you can create infinite sequences, something impossible with arrays:
Copy
Ask AI
// Infinite sequence of natural numbersfunction* naturalNumbers() { let n = 1 while (true) { // Infinite loop! yield n++ }}// This would crash with an array, but generators are lazyconst numbers = naturalNumbers()console.log(numbers.next().value) // 1console.log(numbers.next().value) // 2console.log(numbers.next().value) // 3// We can keep going forever...
You’ll often want to take a limited number of items from an infinite generator:
Copy
Ask AI
// Helper function to take N items from any iterablefunction* take(n, iterable) { let count = 0 for (const item of iterable) { if (count >= n) return yield item count++ }}// Get first 10 Fibonacci numbersconst firstTenFib = [...take(10, fibonacci())]console.log(firstTenFib) // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]// Get first 5 natural numbersconst firstFive = [...take(5, naturalNumbers())]console.log(firstFive) // [1, 2, 3, 4, 5]
Be careful with infinite generators! Never use [...infiniteGenerator()] or for...of on an infinite generator without a break condition. Your program will hang trying to iterate forever.
Copy
Ask AI
// ❌ DANGER — This will hang/crash!const all = [...naturalNumbers()] // Trying to collect infinite items// ✓ SAFE — Use take() or break earlyconst some = [...take(100, naturalNumbers())]
function* chunk(array, size) { for (let i = 0; i < array.length; i += size) { yield array.slice(i, i + size) }}const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]for (const batch of chunk(data, 3)) { console.log('Processing batch:', batch)}// Output:// Processing batch: [1, 2, 3]// Processing batch: [4, 5, 6]// Processing batch: [7, 8, 9]// Processing batch: [10]
This is great for batch processing, API rate limiting, or breaking up heavy computations:
Copy
Ask AI
function* processInBatches(items, batchSize) { for (const batch of chunk(items, batchSize)) { // Process each batch const results = batch.map(item => heavyComputation(item)) yield results }}// Process 1000 items in batches of 100const allItems = new Array(1000).fill(null).map((_, i) => i)for (const batchResults of processInBatches(allItems, 100)) { console.log(`Processed ${batchResults.length} items`) // Could add delay here to avoid blocking the main thread}
async function* fetchAllPages(baseUrl) { let page = 1 let hasMore = true while (hasMore) { const response = await fetch(`${baseUrl}?page=${page}`) const data = await response.json() yield data.items // Yield this page's items hasMore = data.hasNextPage page++ }}// Process all pagesasync function processAllUsers() { for await (const pageOfUsers of fetchAllPages('/api/users')) { console.log(`Processing ${pageOfUsers.length} users...`) for (const user of pageOfUsers) { // Process each user await saveToDatabase(user) } }}
When do you reach for an async generator over Promise.all?
Copy
Ask AI
// Promise.all — All requests in parallel, wait for ALL to completeasync function fetchAllAtOnce(userIds) { const users = await Promise.all( userIds.map(id => fetch(`/api/user/${id}`).then(r => r.json())) ) return users // Returns all users at once}// Async generator — Process as each completesasync function* fetchOneByOne(userIds) { for (const id of userIds) { const user = await fetch(`/api/user/${id}`).then(r => r.json()) yield user // Yield each user as it's fetched }}
Approach
Best for
Promise.all
When you need all results before proceeding
Async generator
When you want to process results as they arrive
Async generator
When fetching everything at once would be too memory-intensive
// ❌ WRONG — This is a regular function, not a generatorfunction myGenerator() { yield 1 // SyntaxError: Unexpected number}// ✓ CORRECT — Note the asteriskfunction* myGenerator() { yield 1}
The asterisk can go next to function or next to the name — both work:
// ❌ WRONG — Nothing happens when you call a generator functionfunction* greet() { console.log('Hello!') yield 'Hi'}greet() // Nothing logged! Returns generator object// ✓ CORRECT — You must call .next() or iterateconst gen = greet()gen.next() // NOW it logs "Hello!"// Or use for...offor (const val of greet()) { console.log(val)}
Mistake 3: Using return instead of yield for iteration values
Copy
Ask AI
// ❌ WRONG — return value won't appear in for...offunction* letters() { yield 'a' yield 'b' return 'c' // This won't be iterated!}console.log([...letters()]) // ['a', 'b'] — no 'c'!// ✓ CORRECT — Use yield for all iteration valuesfunction* letters() { yield 'a' yield 'b' yield 'c'}console.log([...letters()]) // ['a', 'b', 'c']
Mistake 4: Reusing an exhausted generator
Copy
Ask AI
// ❌ WRONG — Generators can only be iterated oncefunction* nums() { yield 1 yield 2}const gen = nums()console.log([...gen]) // [1, 2]console.log([...gen]) // [] — generator is exhausted!// ✓ CORRECT — Create a new generator each timeconsole.log([...nums()]) // [1, 2]console.log([...nums()]) // [1, 2]
Mistake 5: Infinite loop without break condition
Copy
Ask AI
// ❌ DANGER — This will hang your programfunction* forever() { let i = 0 while (true) { yield i++ }}const all = [...forever()] // Infinite loop trying to collect all values!// ✓ SAFE — Use take() or break earlyfunction* take(n, gen) { let count = 0 for (const val of gen) { if (count++ >= n) return yield val }}const firstHundred = [...take(100, forever())] // Safe!
Mistake 6: Using generators when arrays would be simpler
Copy
Ask AI
// ❌ OVERKILL — If you're just returning a fixed list, use an arrayfunction* getDaysOfWeek() { yield 'Monday' yield 'Tuesday' yield 'Wednesday' yield 'Thursday' yield 'Friday' yield 'Saturday' yield 'Sunday'}// ✓ SIMPLER — Just use an arrayconst daysOfWeek = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
Question 1: What's the difference between yield and return in a generator?
Answer:
yieldpauses the generator and returns { value, done: false }. The generator can resume from where it paused.
returnends the generator and returns { value, done: true }. The generator cannot resume.
Important: Values from return are NOT included when using for...of, spread syntax, or Array.from().
Copy
Ask AI
function* example() { yield 'A' // Included in iteration yield 'B' // Included in iteration return 'C' // NOT included in for...of!}console.log([...example()]) // ['A', 'B']
Question 2: How do you make a custom object iterable?
Answer:Add a [Symbol.iterator] method that returns an iterator (an object with a .next() method):
Copy
Ask AI
const myObject = { data: [1, 2, 3], // Method 1: Return an iterator object [Symbol.iterator]() { let index = 0 const data = this.data return { next() { if (index < data.length) { return { value: data[index++], done: false } } return { done: true } } } }}// Method 2: Use a generator (simpler!)const myObject2 = { data: [1, 2, 3], *[Symbol.iterator]() { yield* this.data }}
gen() creates the generator but doesn’t run any code
'Start' logs
First g.next() runs until first yield — logs 'A', returns { value: 1, done: false }
We log the value 1
'Middle' logs
Second g.next() resumes and runs until second yield — logs 'B', returns { value: 2, done: false }
We log the value 2
'C' never logs because we didn’t call g.next() a third time
Question 4: How can you pass values INTO a generator?
Answer:Pass values as arguments to .next(value). The value becomes the result of the yield expression:
Copy
Ask AI
function* adder() { const a = yield 'Enter first number' const b = yield 'Enter second number' yield `Sum: ${a + b}`}const gen = adder()console.log(gen.next().value) // "Enter first number"console.log(gen.next(10).value) // "Enter second number" (a = 10)console.log(gen.next(5).value) // "Sum: 15" (b = 5)
Note: The first .next() starts the generator. Any value passed to it is ignored because there’s no yield waiting to receive it yet.
Question 5: When would you use an async generator?
Answer:Use async generators when you need to yield values from asynchronous operations:
Paginated APIs — Fetch and yield page by page
Streaming data — Process chunks as they arrive
Database cursors — Iterate through large result sets
File processing — Read and yield lines from large files
Copy
Ask AI
async function* fetchPages(url) { let page = 1 while (true) { const response = await fetch(`${url}?page=${page}`) const data = await response.json() if (data.items.length === 0) return yield data.items page++ }}// Consume with for await...offor await (const items of fetchPages('/api/products')) { processItems(items)}
Question 6: Why can't you use [...infiniteGenerator()]?
Answer:Spread syntax (...) tries to collect ALL values into an array. With an infinite generator, this means infinite iteration. Your program will hang trying to collect infinite values.
Copy
Ask AI
function* forever() { let i = 0 while (true) yield i++}// ❌ DANGER — Hangs forever!const all = [...forever()]// ✓ SAFE — Limit how many you takefunction* take(n, gen) { let i = 0 for (const val of gen) { if (i++ >= n) return yield val }}const first100 = [...take(100, forever())]
Always use a limiting function like take(), or manually call .next() a specific number of times.
A generator function, declared with function*, is a special function that can pause its execution with yield and resume later. Each call to the generator’s .next() method runs the function until the next yield and returns the yielded value. Generators were introduced in ECMAScript 2015 and are defined by the iteration protocols in the specification.
What is the difference between yield and return in a generator?
yield pauses the generator and produces a value, but the generator can be resumed to continue execution. return terminates the generator permanently and sets done: true in the result object. A yielded value has done: false, while a returned value has done: true. Values produced by return are not included in for...of loops.
What are iterators in JavaScript?
An iterator is an object that implements the iterator protocol — it has a .next() method that returns { value, done } objects. As documented on MDN, many built-in JavaScript types are iterable (Arrays, Strings, Maps, Sets), meaning they have a Symbol.iterator method that returns an iterator. Generators automatically create iterators.
What is lazy evaluation in JavaScript generators?
Lazy evaluation means values are computed only when requested, not upfront. Generators are inherently lazy — they compute each value on demand when .next() is called. This is memory-efficient because you never hold the entire sequence in memory. It also enables infinite sequences, where computing all values upfront would be impossible.
What are async generators and when should you use them?
Async generators combine async function* syntax with yield to produce values asynchronously. They are consumed with for await...of loops and are ideal for streaming data from APIs, reading files line by line, or paginating through large datasets. According to MDN, async generators were standardized in ECMAScript 2018 as part of the async iteration proposal.