Generator Functions
Pausable Functions with `yield`
LinkedIn Hook
A normal function runs start to finish in one go. You call it, it finishes, it returns.
A generator function doesn't. It can pause in the middle, hand you a value, and wait. You ask for the next value — it picks up exactly where it stopped. And it does this as many times as you want.
That single property unlocks things regular functions can't touch: infinite sequences that don't blow up your memory, lazy pipelines that process a million items by only computing what you actually consume, custom iterables that work with
for...ofand spread, and the control-flow pattern that eventually becameasync/await.In this lesson I walk through
function*,yield,yield*,.next(value), practical infinite generators, lazy pipelines, and async generators withfor await...of.If generators have always felt like magic — this is where the magic becomes mechanism.
Read the full lesson -> [link]
#JavaScript #InterviewPrep #Generators #Iterators #AsyncAwait #Frontend #WebDevelopment
What You'll Learn
- What generator functions are and how
function*/yield/.next()work together - How generators implement the iterator protocol (
for...of, spread, destructuring) - The difference between
yield,return, andyield*, and how.next(value)sends data IN - How to build infinite sequences, lazy pipelines, and async generators that stream data
The Bookmark Analogy
Imagine a book with a bookmark. A normal function is like reading the entire book in one sitting — start to finish, no breaks. A generator is like reading one chapter at a time, putting in a bookmark, and coming back later to continue exactly where you left off.
What are Generators?
Generators are special functions that can pause and resume their execution. They use function* syntax and the yield keyword.
// Regular function — runs to completion
function regular() {
console.log("A");
console.log("B");
console.log("C");
return "done";
}
regular(); // A, B, C — all at once, returns "done"
// Generator function — can pause at each yield
function* generator() {
console.log("A");
yield 1;
console.log("B");
yield 2;
console.log("C");
return 3;
}
const gen = generator(); // Nothing happens! Just creates the generator object
gen.next(); // "A" -> { value: 1, done: false }
gen.next(); // "B" -> { value: 2, done: false }
gen.next(); // "C" -> { value: 3, done: true }
gen.next(); // { value: undefined, done: true }
Key points:
- Calling a generator function does NOT execute its body — it returns a generator object
- Each
.next()call runs until the nextyieldand pauses yieldsends a value out AND pauses executionreturnsends the final value withdone: true
The Iterator Protocol
Generators automatically implement the iterator protocol — meaning they work with for...of, spread operator, and destructuring.
function* colors() {
yield "red";
yield "green";
yield "blue";
}
// for...of loop
for (const color of colors()) {
console.log(color);
}
// "red", "green", "blue"
// Spread operator
const colorArray = [...colors()];
console.log(colorArray); // ["red", "green", "blue"]
// Destructuring
const [first, second] = colors();
console.log(first, second); // "red" "green"
// Array.from
const arr = Array.from(colors());
console.log(arr); // ["red", "green", "blue"]
Important: for...of ignores the return value — it stops when done is true but doesn't include that final value.
function* withReturn() {
yield 1;
yield 2;
return 3; // this value is NOT included in for...of
}
console.log([...withReturn()]); // [1, 2] — NOT [1, 2, 3]!
yield vs return
function* comparison() {
yield 1; // pauses, can resume, { value: 1, done: false }
yield 2; // pauses, can resume, { value: 2, done: false }
return 3; // ENDS the generator, { value: 3, done: true }
yield 4; // NEVER reached — dead code after return
}
const gen = comparison();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }
| Feature | yield | return |
|---|---|---|
| Pauses execution? | Yes | No — terminates |
done value | false | true |
| Can resume after? | Yes | No |
Included in for...of? | Yes | No |
| Can be used multiple times? | Yes | Only once |
Passing Values Back In — generator.next(value)
This is where generators get really interesting. You can send values into a generator via .next(value):
function* conversation() {
const name = yield "What is your name?";
const age = yield `Hello ${name}! How old are you?`;
return `${name} is ${age} years old.`;
}
const chat = conversation();
console.log(chat.next());
// { value: "What is your name?", done: false }
console.log(chat.next("Rakibul"));
// { value: "Hello Rakibul! How old are you?", done: false }
console.log(chat.next(25));
// { value: "Rakibul is 25 years old.", done: true }
How it works:
- First
.next()runs until the firstyield— the value passed to the first.next()is always ignored - Second
.next("Rakibul")— the string"Rakibul"becomes the result of the firstyieldexpression - Third
.next(25)— the number25becomes the result of the secondyieldexpression
// Another example: accumulator
function* accumulator() {
let total = 0;
while (true) {
const value = yield total;
total += value;
}
}
const acc = accumulator();
console.log(acc.next()); // { value: 0, done: false } — initial
console.log(acc.next(5)); // { value: 5, done: false }
console.log(acc.next(10)); // { value: 15, done: false }
console.log(acc.next(3)); // { value: 18, done: false }
yield Delegation (yield*)
yield* delegates to another iterable (another generator, an array, a string, etc.):
function* inner() {
yield "a";
yield "b";
}
function* outer() {
yield 1;
yield* inner(); // delegate to inner generator
yield 2;
}
console.log([...outer()]); // [1, "a", "b", 2]
// yield* works with any iterable
function* withIterable() {
yield* [10, 20, 30]; // delegate to array
yield* "hello"; // delegate to string
}
console.log([...withIterable()]);
// [10, 20, 30, "h", "e", "l", "l", "o"]
yield* also captures the return value of the delegated generator:
function* inner() {
yield "x";
return "INNER_RETURN";
}
function* outer() {
const result = yield* inner();
console.log("Inner returned:", result);
yield "done";
}
const gen = outer();
console.log(gen.next()); // { value: "x", done: false }
console.log(gen.next()); // "Inner returned: INNER_RETURN"
// { value: "done", done: false }
Practical Generators — Infinite Sequences, Lazy Evaluation
1. Infinite ID Generator
function* idGenerator(start = 1) {
let id = start;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3
// Goes on forever — but only computes on demand!
2. Range Generator (Python-like range)
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
console.log([...range(0, 10)]); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8]
console.log([...range(5, 0, -1)]); // [] — need to handle negative step
// Fixed range that handles negative steps
function* rangeFull(start, end, step = start < end ? 1 : -1) {
if (step > 0) {
for (let i = start; i < end; i += step) yield i;
} else {
for (let i = start; i > end; i += step) yield i;
}
}
console.log([...rangeFull(5, 0)]); // [5, 4, 3, 2, 1]
3. Fibonacci Generator
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Get first 10 fibonacci numbers
const fib = fibonacci();
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Utility: take first N from any generator
function take(n, gen) {
const result = [];
for (const val of gen) {
result.push(val);
if (result.length === n) break;
}
return result;
}
console.log(take(7, fibonacci())); // [0, 1, 1, 2, 3, 5, 8]
4. Lazy Pipeline Processing
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
// Process a million items lazily — no intermediate arrays!
const numbers = range(1, 1000001);
const evens = filter(numbers, (n) => n % 2 === 0);
const squared = map(evens, (n) => n * n);
// Only computes values as needed
const firstFive = take(5, squared);
console.log(firstFive); // [4, 16, 36, 64, 100]
// Only processed 10 numbers total — not 1 million!
Async Generators
Combine generators with async/await for streaming data:
// Async generator — yields promises
async function* fetchPages(baseUrl, totalPages) {
for (let page = 1; page <= totalPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data;
}
}
// Consume with for-await-of
async function processAllPages() {
for await (const pageData of fetchPages("/api/users", 10)) {
console.log("Processing page:", pageData);
// Process one page at a time — no memory overload
}
}
// Real-world: streaming log lines
async function* readLines(stream) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line in buffer
for (const line of lines) {
yield line;
}
}
if (buffer) yield buffer;
} finally {
reader.releaseLock();
}
}
Common Mistakes
- Expecting
function* gen() { ... }; gen();to run the body. It doesn't. Calling a generator function returns a paused generator object — nothing inside the body runs until you call.next(). - Trying to
yieldfrom inside a nested arrow function or callback within a generator.yieldonly works directly inside the generator function body — the inner arrow isn't a generator. - Assuming
[...gen()]captures thereturnvalue. It doesn't. Spread andfor...ofstop whendone: true, ignoring the final returned value. Usegen.next()manually oryieldthe value instead of returning it if you need it.
Interview Questions
Q: What is a generator function and how is it different from a regular function?
A generator function (declared with
function*) can pause its execution withyieldand resume later with.next(). Unlike regular functions which run to completion, generators are lazy — they produce values on demand. They return a generator object that implements the iterator protocol.
Q: Implement an infinite sequence generator for unique IDs.
function* uniqueId(prefix = "id") { let counter = 0; while (true) { yield `${prefix}_${counter++}`; } } const userIds = uniqueId("user"); console.log(userIds.next().value); // "user_0" console.log(userIds.next().value); // "user_1"
Q: What does yield* do?
yield*delegates iteration to another iterable. It yields each value from the delegated iterable as if the outer generator had yielded them directly. It also captures and returns the delegated generator's return value.
Q: Can you pass values into a generator? How?
Yes, via
.next(value). The value passed to.next()becomes the result of theyieldexpression inside the generator. The first.next()call's argument is always discarded because there is noyieldexpression waiting to receive it.
Q: What was the role of generators before async/await?
Before async/await (ES2017), generators combined with Promises were used for async flow control. Libraries like
cowould automatically call.next()on a generator, passing resolved promise values back in:// Pre-async/await pattern with co library co(function* () { const user = yield fetchUser(1); const posts = yield fetchPosts(user.id); return posts; }); // Is essentially what async/await compiles to: async function () { const user = await fetchUser(1); const posts = await fetchPosts(user.id); return posts; }
Q: function* vs async function*?
function*yields values synchronously and is consumed withfor...of, spread, or.next().async function*canawaitinside the body and yield promises; it's consumed withfor await...of.
Q: Can you use yield inside a callback within a generator?
No.
yieldcan only be used directly inside the generator function body, not inside nested callbacks or arrow functions.
Q: What happens when you call .return() on a generator?
It terminates the generator, running any
finallyblocks along the way, and returns{ value: <arg>, done: true }.
Q: Can generators be used to implement custom iterables?
Yes. You can define
[Symbol.iterator]as a generator method on any object.const customRange = { from: 1, to: 5, *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) { yield i; } }, }; console.log([...customRange]); // [1, 2, 3, 4, 5]
Quick Reference — Cheat Sheet
GENERATORS — QUICK MAP
function* gen() { const g = gen();
yield 1; <-> g.next() -> {value:1, done:false}
yield 2; <-> g.next() -> {value:2, done:false}
return 3; <-> g.next() -> {value:3, done:true}
}
Keyword cheatsheet:
yield -> pause + send value OUT (done: false)
return -> terminate + final value (done: true)
yield* -> delegate to another iterable
.next(v) -> resume + send value IN
.return() -> force terminate, run finally blocks
.throw(e) -> throw inside generator at paused yield
Works natively with:
for...of, [...spread], destructuring, Array.from
Variants:
function* -> sync generator (for...of)
async function* -> async generator (for await...of)
Use cases:
- Infinite sequences (ids, fibonacci)
- Lazy pipelines (map/filter without arrays)
- Custom iterables (Symbol.iterator)
- Streaming async data (paged APIs, log tails)
Previous: Memoization -> Cache Your Answers, Never Recompute Next: Shallow vs Deep Copy -> The Reference Trap
This is Lesson 6.9 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.