What is an Iterator?
An iterator is an object that lets you go through a list (or any collection) one item at a time.
It gives you the next item each time you ask, and remembers where it stopped, like a bookmark.
You don’t need to know how the collection is stored, just that you can use the iterator to step through it.
The Iterator Protocol
In JavaScript, an iterator is an object that implements the iterator protocol by having a next()
method.
The next()
method returns an object with two properties:
value
: the next value from the listdone
: a boolean indicating whether the iteration is complete
Example of Iterator
function createIterator(arr) {
let index = 0;
return {
next: function () {
if (index < arr.length) {
return { value: arr[index++], done: false };
} else {
return { value: undefined, done: true };
}
},
};
}
const numbers = createIterator([10, 20, 30]);
console.log(numbers.next()); // { value: 10, done: false }
console.log(numbers.next()); // { value: 20, done: false }
console.log(numbers.next()); // { value: 30, done: false }
console.log(numbers.next()); // { value: undefined, done: true }
Here,
- You define a function
createIterator
that takes an array (arr
) and returns an iterator object. let index = 0;
keep track of which element to return next.return { next: function () {
returns an object with anext()
method, this follows the iterator protocol.if (index < arr.length) {...} else {...}
:- If there are more items in the array:
- Return the current item and
done: false
- Move to the next item by increasing index
- Return the current item and
- If there are no more items:
- Return
value: undefined
anddone: true
- Return
- If there are more items in the array:
- Using the Iterator:
const numbers = createIterator([10, 20, 30]);
creates an iterator from the array[10, 20, 30]
.
- Calling
next()
:console.log(numbers.next()); // { value: 10, done: false }
:- Each call to
next()
gives the next value. - Once the end is reached,
done: true
tells you there’s nothing left.
- Each call to
- Result: You get each item from the array, one at a time, in order, with full control over the process.
Why use it?
Custom iterators give you full control over how and when items are returned. It's great when working with custom data structures or when you want to pause/resume iteration.
The Symbol.iterator Protocol
You can make any object iterable by defining [Symbol.iterator]()
.
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
},
};
},
};
for (const num of range) {
console.log(num); // Output: 1 2 3 4 5
}
Here,
range
is a plain object withstart
andend
properties.[Symbol.iterator]() { ... }
:- By adding a
[Symbol.iterator]()
method, you makerange
work withfor...of
.
- By adding a
- Inside
Symbol.iterator
:- You initialize
current
to start fromrange.start
(1). end
is the last number in therange
(5).
- You initialize
- Returning the iterator object:
return {...};
- This object has a
next()
method. - Each call to
next()
returns the next number and setsdone
tofalse
. - When
current
exceedsend
, it returnsdone: true
.
- This object has a
- Using in a
for...of
loop:for (const num of range) {...}
- Because
range
now follows the iterable protocol, it can be looped withfor...of
.
- Because
- Result: The object behaves like an array in loops, you can control how it yields values!
What is a Generator?
A generator function is a special type of function that can pause and resume its execution using the yield
keyword.
Instead of returning all values at once, it produces a sequence of values over time, one at a time.
Syntax of Generator
function* generatorName() {
yield value1;
yield value2;
}
- Notice the
*
after thefunction
keyword, this marks it as a generator function. yield
is used to pause and send a value back each time the function runs.
Example:
function* countUp() {
yield 1;
yield 2;
yield 3;
}
const counter = countUp();
console.log(counter.next().value); // Output: 1
console.log(counter.next().value); // Output: 2
console.log(counter.next().value); // Output: 3
Here,
countUp
is a generator function that yields three values: 1, 2, and 3.- Calling
countUp()
does not run the function right away, it returns a generator objectcounter
. - Calling
counter.next()
:- Runs the function until the next yield.
- Returns an object
{ value: X, done: false }
where value is the yielded value.
counter.next().value
gets the current yielded value.- Each call to
.next()
resumes where it last stopped. - After the last yield,
.next()
would return{ value: undefined, done: true }
indicating completion.
Use Cases of Generator
-
Custom Iteration Logic:
Generators are useful when you want to:
- Create custom iterators
- Generate sequences step-by-step
- Handle async flows (when used with async/await or Promises)
Example: Iterating with
for...of
function* names() { yield "Alice"; yield "Bob"; yield "Charlie"; } for (let name of names()) { console.log(name); }
Here,
-
function* names() { ... }
- This defines a generator function named
names
. - The
*
after function tells JavaScript that it’s a generator.
- This defines a generator function named
-
yield "Alice";, yield "Bob";, yield "Charlie";
- Each
yield
gives out one value when the generator runs. - But instead of giving all values at once, it pauses after each yield.
- Each
-
names()
- This doesn't run the code immediately.
- It returns a generator object that can be used to get values one by one.
-
for (let name of names()) { ... }
-
The
for...of
loop that works well with generators. It automatically:- Starts the generator,
- Gets the first yield value ("Alice"),
- Then resumes to get "Bob",
- Then resumes again to get "Charlie",
- Finally, it stops when the generator is done.
-
The
-
console.log(name);
- This line runs inside the loop each time a value is yielded.
- It prints each name one by one.
Output:
Alice Bob Charlie
Generator Methods
1. next(value)
You can pass values back into a generator using .next(value)
. This makes generators interactive, useful for prompts, streams, or step-based workflows.
function* echo() {
const input = yield "What's your name?";
yield `Hello, ${input}!`;
}
const gen = echo();
console.log(gen.next()); // Output: { value: "What's your name?", done: false }
console.log(gen.next("Alice")); // Output: { value: "Hello, Alice!", done: false }
console.log(gen.next()); // Output: { value: undefined, done: true }
Here,
function* echo()
defines a generator.yield
"What's your name?" pauses the function and sends the first message out.const gen = echo();
creates the generator object, but doesn’t run the function yet.console.log(gen.next());
starts the generator and runs until the first yield. So the output is{ value: "What's your name?", done: false }
.console.log(gen.next("Alice"));
resumes the generator, sending "Alice" as the result of the first yield and assigns "Alice" to input. So the output is{ value: "Hello, Alice!", done: false }
.console.log(gen.next());
resumes the generator, no more yield statements left, so it finishes. So the output is{ value: undefined, done: true }
.
2. return(value)
The .return(value)
method immediately stops the generator and sets the done
flag to true
. It also returns the provided value.
This is useful if you want to exit early from the generator before it yields all values.
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const gen = numbers();
console.log(gen.next()); // Output: { value: 1, done: false }
console.log(gen.return(42)); // Output: { value: 42, done: true }
console.log(gen.next()); // Output: { value: undefined, done: true }
Here,
- First,
.next()
gives the first value 1. - Then,
.return(42)
forces the generator to finish immediately, returning{ value: 42, done: true }
. - Any further
.next()
calls after.return()
just return{ value: undefined, done: true }
.
3. throw(error)
You can use .throw(error)
to throw an error inside the generator at the current yield point.
If the generator has a try...catch
block, the error can be caught and handled gracefully.
function* errorHandler() {
try {
yield 1;
yield 2;
} catch (e) {
console.log("Caught:", e.message);
yield "error handled";
}
}
const gen = errorHandler();
console.log(gen.next()); // Output: { value: 1, done: false }
console.log(gen.throw(new Error("Oops!"))); // Logs: "Caught: Oops!", returns { value: "error handled", done: false }
Here,
gen.next()
starts the generator and yields 1.gen.throw(new Error("Oops!"))
throws an error at the second yield.- The error is caught in the
catch
block, "Caught: Oops!" is logged. - The generator yields "error handled" as the next value.
Delegating to Other Generators
Sometimes, you want a generator to include values from another generator.
Instead of calling them one by one, use yield*
to delegate control.
function* firstHalf() {
yield 1;
yield 2;
}
function* secondHalf() {
yield 3;
yield 4;
}
function* combined() {
yield* firstHalf(); // Delegates to firstHalf()
yield* secondHalf(); // Then delegates to secondHalf()
}
for (let num of combined()) {
console.log(num); // Output: 1, 2, 3, 4
}
Here,
combined()
doesn’t yield directly. It hands over control tofirstHalf()
usingyield*
.- After
firstHalf()
finishes, control goes tosecondHalf()
via anotheryield*
. - The
for...of
loop receives values from all delegated generators in order.
👉 Next tutorial: Garbage Collection