I ignored JavaScript generators for years. They looked weird, the docs were terrible, and every example showed Fibonacci ( which nobody actually writes ). Then I needed to process a 2GB CSV file in the browser without crashing the tab, and generators suddenly made sense.

Here is what actually matters about them.

The Basics in 30 Seconds

A generator is a function that can pause. You write function* instead of function, and use yield to return values one at a time. The caller resumes it with .next(). That is it.

function* countTo3() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = countTo3();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }

No callbacks, no promises, no event loop tricks. Just pause and resume.

Ocean waves
Generators are like waves - they pause and resume. Not the best metaphor, but you get the idea.

Why I Actually Use Them

Lazy iteration on large data. That is 90% of my use case. Instead of loading an entire array into memory, I yield one item at a time. The consumer decides when to stop.

// Process a huge file line by line
function* readLines(text) {
  const lines = text.split("\n");
  for (const line of lines) {
    yield line;
  }
}

// Only process first 100 lines, rest never evaluated
for (const line of readLines(hugeText)) {
  if (processed >= 100) break;
  processLine(line);
}

That break statement is the key. The generator stops. No wasted work, no excess memory. Try doing that with a regular function that returns a full array.

Coastal landscape
When your generator actually works on the first try ( it never does )

yield* for Delegation

The yield* syntax delegates to another generator. I use this to compose pipelines ( like combining multiple data sources into one stream ).

function* users() {
  yield { name: "Davide", city: "Montevideo" };
  yield { name: "Ana", city: "Rome" };
}

function* products() {
  yield { id: 1, price: 29 };
  yield { id: 2, price: 49 };
}

function* allData() {
  yield* users();
  yield* products();
}

for (const item of allData()) {
  console.log(item);
}

Clean, readable, no nesting. Just streams feeding into streams.

Abstract pattern
Yield* in action: delegating like I delegate all my bugs to the intern

Passing Values Back In

This is the part most people miss. .next(value) sends a value back into the generator. The yielded expression receives it.

function* askName() {
  const name = yield "What is your name?";
  yield `Hello, ${name}`;
}

const gen = askName();
gen.next();           // { value: "What is your name?", done: false }
gen.next("Davide");  // { value: "Hello, Davide", done: false }
gen.next();          // { value: undefined, done: true }

I use this for state machines. Each yield is a state, and the input from .next() determines the transition. Way cleaner than a switch statement with 15 cases.

Async Generators ( the Real Power )

This is where generators finally click for most people. Combine function* with async/await and you can yield values from asynchronous sources.

async function* fetchPages(url) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const res = await fetch(`${url}?page=${page}`);
    const data = await res.json();
    yield data.items;
    hasMore = data.hasMore;
    page++;
  }
}

// Consume one page at a time
for await (const items of fetchPages("/api/products")) {
  renderItems(items);
  if (items.length === 0) break;
}

That for await...of loop is the killer feature. No manual .next() calls, no callback hell. Just iterate over async results like they were a normal array.

Mountain landscape
Async generators: because regular generators were not confusing enough

What I Do Not Use Them For

Generators are not a replacement for everything. I do not use them for simple list transformations ( use .map()/.filter() ), I do not use them as coroutine libraries ( use async/await ), and I do not use them for event emitters ( use EventTarget ).

They are a specific tool for lazy, pauseable iteration. Use them for that and they are great. Use them for everything and your code becomes unreadable.

3 Rules I Follow

1. Always return an iterator protocol. Generators already implement Symbol.iterator, so they work with for...of and spread syntax out of the box.

2. Name them clearly. I prefix with the data source: readLines, fetchPages, walkTree. No generic names like process.

3. Always handle early termination. If someone breaks out of a for...of loop, the generator stays suspended. Add a try/finally if you need cleanup ( like closing a file handle ).

function* readWithCleanup(file) {
  try {
    while (file.hasMore()) {
      yield file.readLine();
    }
  } finally {
    file.close(); // runs even if consumer breaks early
  }
}

Generators are not complicated. The docs just make them look that way. Start with lazy iteration, add async when you need it, and ignore everything else until you actually have a use for it. :)