JavaScript Basics: Consume Asynchronous Sequences with for await
An asynchronous iterator supplies its next result through a promise. for await...of waits for each result, making it useful for paginated data and sequential processing. Unlike a synchronous generator, an async producer can wait for work between values without blocking the event loop.
These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See MDN’s for await…of reference for the async iterator protocol and early-exit behavior.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Flatten asynchronously fetched pages
Save this as consume-async-pages.mjs:
import assert from 'node:assert/strict';
import { setTimeout as delay } from 'node:timers/promises';
const pages = [['red', 'green'], ['blue'], []];
let requests = 0;
async function fetchPage(index) {
requests += 1;
await delay(1);
return pages[index] ?? [];
}
async function* items() {
for (let index = 0; ; index += 1) {
const page = await fetchPage(index);
if (page.length === 0) return;
for (const item of page) yield item;
}
}
const received = [];
for await (const item of items()) received.push(item);
assert.deepEqual(received, ['red', 'green', 'blue']);
assert.equal(requests, 3);
console.log(received.join(', '));
console.log(`Page requests: ${requests}`);Run it:
node consume-async-pages.mjsOutput:
red, green, blue
Page requests: 3The local delayed function simulates page fetching without network access. This example uses an empty page as its stop signal; a real API might use a next-page token instead. Each later page is requested only after the current page’s items are consumed. The small result array makes testing easy; a real consumer can process each item immediately.
2. Wait for cleanup when stopping early
Save this as await-iterator-close.mjs:
import assert from 'node:assert/strict';
import { setTimeout as delay } from 'node:timers/promises';
let closed = false;
let pulls = 0;
const source = {
[Symbol.asyncIterator]() { return this; },
async next() {
pulls += 1;
return { value: pulls, done: false };
},
async return() {
await delay(1);
closed = true;
return { done: true };
},
};
const received = [];
for await (const item of source) {
received.push(item);
if (item === 2) break;
}
assert.deepEqual(received, [1, 2]);
assert.equal(pulls, 2);
assert.equal(closed, true);
console.log(`Pulled: ${pulls}`);
console.log(`Cleanup finished: ${closed}`);Run it:
node await-iterator-close.mjsOutput:
Pulled: 2
Cleanup finished: trueThis explicit iterator shows both protocol methods: next() produces a promised result, and return() releases resources on early exit. The loop awaits return() before continuing after break. No third value is requested. An iterator must implement the cleanup it needs; stopping a loop does not automatically cancel unrelated background tasks.
3. Catch a producer failure and verify cleanup
Save this as handle-async-source-error.mjs:
import assert from 'node:assert/strict';
import { setTimeout as delay } from 'node:timers/promises';
let cleaned = false;
async function* readings() {
try {
yield 21;
await delay(1);
throw new Error('Sensor unavailable');
} finally {
await delay(1);
cleaned = true;
}
}
const received = [];
let message;
try {
for await (const value of readings()) received.push(value);
} catch (error) {
message = error.message;
}
assert.deepEqual(received, [21]);
assert.equal(message, 'Sensor unavailable');
assert.equal(cleaned, true);
console.log(`Received: ${received.join(', ')}`);
console.log(message);
console.log(`Producer cleanup: ${cleaned}`);Run it:
node handle-async-source-error.mjsOutput:
Received: 21
Sensor unavailable
Producer cleanup: trueA rejected next-result promise becomes an exception at the loop. Here the generator’s own finally handles cleanup before the rejection reaches the consumer. Do not assume that every custom iterator gets an automatic return() call when its next() rejects. Put resource cleanup inside the producer as appropriate, and await asynchronous work in the loop body when it must remain sequential.
Clean up
In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:
rm -- consume-async-pages.mjs await-iterator-close.mjs handle-async-source-error.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.