Blog

for await…of and Symbol.asyncIterator in JavaScript

Some sequences arrive over time. Pages from an API, lines from a file, messages from a socket. You know there is more coming, but you have to wait for it.

for await...of handles that shape. It works with Symbol.asyncIterator, which is the same idea as Symbol.iterator except that next() returns a promise.

An async generator

async function* gives you an async iterable with no manual protocol work. Each yield can come after an await.

const sleep = ms => new Promise(r => setTimeout(r, ms));

// async generator
async function* ticker(count) {
  for (let i = 1; i <= count; i++) {
    await sleep(10);
    yield i;
  }
}
for await (const tick of ticker(3)) console.log('tick', tick);

It prints:

tick 1
tick 2
tick 3

The loop waits for each value before running the body. There is no callback and no queue to manage.

Paging over an API

The common real case is a paged endpoint. The object below pretends each page costs a network call.

// Symbol.asyncIterator on an object
const pager = {
  pages: [['a', 'b'], ['c', 'd'], ['e']],
  async *[Symbol.asyncIterator]() {
    for (const page of this.pages) {
      await sleep(10);            // pretend this is a network call
      yield page;
    }
  }
};
for await (const page of pager) console.log('page', page);

// collect everything
const all = [];
for await (const page of pager) all.push(...page);
console.log(all);

It prints:

page [ 'a', 'b' ]
page [ 'c', 'd' ]
page [ 'e' ]
[ 'a', 'b', 'c', 'd', 'e' ]

The consumer does not know or care how many pages there are. If you want everything at once, collect it in the loop.

It is a separate protocol

An async iterable does not work with plain for...of.

// for...of on an async iterable fails
try { for (const x of pager) console.log(x); }
catch (err) { console.log(err.constructor.name + ':', err.message); }

It prints:

TypeError: pager is not iterable

An object can implement both symbols if you want it to work either way, but they are different methods.

Breaking early cleans up

When you break out of the loop, the generator is closed and its finally block runs. That is where you release the connection or the file handle.

// break runs the generator's finally block
async function* withCleanup() {
  try { yield 1; yield 2; yield 3; }
  finally { console.log('cleanup ran'); }
}
for await (const n of withCleanup()) {
  console.log('got', n);
  if (n === 2) break;
}

It prints:

got 1
got 2
cleanup ran

Values 1 and 2 arrive, the break stops the loop, and cleanup runs before the loop exits. The third value is never produced.

What to remember

  • for await...of looks for Symbol.asyncIterator.
  • async function* is the easy way to write one.
  • A plain for...of loop will not accept an async iterable.
  • break triggers the generator finally block, so cleanup is reliable.

Node streams already implement this protocol, so for await (const chunk of stream) works with no wrapper. Your own paged clients can look exactly the same to the code that uses them.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply