JavaScript Interview Prep
Functions Deep Dive

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...of and spread, and the control-flow pattern that eventually became async/await.

In this lesson I walk through function*, yield, yield*, .next(value), practical infinite generators, lazy pipelines, and async generators with for 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


Generator Functions thumbnail


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, and yield*, 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 next yield and pauses
  • yield sends a value out AND pauses execution
  • return sends the final value with done: 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 }
Featureyieldreturn
Pauses execution?YesNo — terminates
done valuefalsetrue
Can resume after?YesNo
Included in for...of?YesNo
Can be used multiple times?YesOnly 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:

  1. First .next() runs until the first yield — the value passed to the first .next() is always ignored
  2. Second .next("Rakibul") — the string "Rakibul" becomes the result of the first yield expression
  3. Third .next(25) — the number 25 becomes the result of the second yield expression
// 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!

Generator Functions visual 1


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 yield from inside a nested arrow function or callback within a generator. yield only works directly inside the generator function body — the inner arrow isn't a generator.
  • Assuming [...gen()] captures the return value. It doesn't. Spread and for...of stop when done: true, ignoring the final returned value. Use gen.next() manually or yield the 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 with yield and 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 the yield expression inside the generator. The first .next() call's argument is always discarded because there is no yield expression 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 co would 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 with for...of, spread, or .next(). async function* can await inside the body and yield promises; it's consumed with for await...of.

Q: Can you use yield inside a callback within a generator?

No. yield can 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 finally blocks 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.

On this page