JavaScript
async generators
for-await-of
generator functions
programming issues

Generator return doesn't work in for-await-of loop

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In JavaScript, for await ... of consumes values that are yielded by an async iterator. It does not expose the final value passed to return. That behavior surprises people because the generator really does return a value, but the loop protocol treats that final step as termination, not as one more item.

What the Loop Actually Reads

An async generator produces iteration results shaped like value plus done. As long as done is false, the loop body runs. When done becomes true, the loop stops immediately.

That means this generator returns a value, but the loop never prints it:

javascript
1async function* buildSequence() {
2  yield "step-1";
3  yield "step-2";
4  return "final-state";
5}
6
7async function run() {
8  for await (const item of buildSequence()) {
9    console.log(item);
10  }
11}
12
13run();

The output is:

text
step-1
step-2

The "final-state" value exists only on the final iterator result where done is true.

Why return Feels Different From yield

Think of yield as publishing data to the consumer and return as ending the iterator. The returned value is part of the iterator protocol, but for await ... of is intentionally designed to hide that final record and stop.

This is consistent with synchronous generators too. The same distinction exists there, but async iteration makes it easier to forget because the code often looks like a stream API.

How to Read the Final Return Value

If you need both yielded values and the final returned value, iterate manually with next.

javascript
1async function* buildSequence() {
2  yield "step-1";
3  yield "step-2";
4  return "final-state";
5}
6
7async function readManually() {
8  const iterator = buildSequence();
9
10  while (true) {
11    const result = await iterator.next();
12    if (result.done) {
13      console.log("return value:", result.value);
14      break;
15    }
16    console.log("yielded:", result.value);
17  }
18}
19
20readManually();

That code prints both the streamed values and the terminal payload.

Prefer a Final yield for Consumer-Facing Data

If downstream code is supposed to use for await ... of, the cleanest design is usually to yield the final information explicitly rather than burying it in return.

javascript
1async function* taskEvents() {
2  yield { type: "progress", value: 25 };
3  yield { type: "progress", value: 75 };
4  yield { type: "done", summary: "completed" };
5}
6
7async function consume() {
8  for await (const event of taskEvents()) {
9    console.log(event);
10  }
11}
12
13consume();

This makes the contract obvious: everything the consumer needs arrives through yielded values.

Cleanup and Early Exit

return has another important role in generators: cleanup. If the consumer stops early, the generator can still run finally blocks.

javascript
1async function* source() {
2  try {
3    yield 1;
4    yield 2;
5    yield 3;
6  } finally {
7    console.log("cleanup");
8  }
9}
10
11async function stopEarly() {
12  for await (const n of source()) {
13    console.log(n);
14    if (n === 2) {
15      break;
16    }
17  }
18}
19
20stopEarly();

This is why return is still useful even when its value is not visible in the loop body. It remains part of how iterators shut down correctly.

A Utility Pattern for Capturing Both

If you regularly need both the emitted values and the final return value, write a helper that drives the iterator manually and packages the result.

javascript
1async function collect(iterator) {
2  const items = [];
3
4  while (true) {
5    const step = await iterator.next();
6    if (step.done) {
7      return { items, final: step.value };
8    }
9    items.push(step.value);
10  }
11}
12
13async function* producer() {
14  yield "chunk-1";
15  yield "chunk-2";
16  return "checksum-ok";
17}
18
19collect(producer()).then(console.log);

This pattern is helpful when the final value is metadata such as a checksum, summary, or completion status.

Common Pitfalls

A common mistake is putting business-critical output into return and then consuming the iterator with for await ... of. That output disappears from the loop body by design.

Another mistake is assuming async generators behave like an event emitter. They do not. The consumer sees only yielded steps unless it manually inspects the final iterator result.

It is also easy to forget cleanup. If the generator owns resources, use try and finally so early loop termination does not leak them.

Summary

  • 'for await ... of reads yielded values only, not the final returned value.'
  • The final return payload is available only on the iterator result where done is true.
  • If you need that payload, iterate manually with next.
  • For consumer-visible completion data, prefer a final yield rather than return.
  • Use finally blocks in generators to keep cleanup reliable.

Course illustration
Course illustration

All Rights Reserved.