异步迭代器与通过 for-await-of 遍历流
使用异步迭代便捷地消费可读流,并在流与异步生成器之间进行转换。
异步迭代器与通过 for-await-of 遍历流 是 CoddyKit 上的免费 Node.js Backend Development Bootcamp 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Node.js Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Node.js Backend Development Bootcamp 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Async Iteration Over Streams
Node.js Readable streams emit chunks over time. The classic way to consume them is by wiring up 'data', 'end', and 'error' event listeners. That works, but it scatters your logic across callbacks and makes pausing, resuming, and error handling awkward.
Since Node 10, every Readable stream is an async iterable. That means you can consume it with a plain for await...of loop:
- Sequential chunk processing, written top to bottom
- Automatic backpressure — the loop pulls one chunk at a time
- try/catch works for stream errors
This lesson shows how to consume, transform, and produce streams ergonomically with async iteration.
for-await-of Basics
A for await...of loop awaits each value produced by an async iterator before moving to the next. When applied to a Readable stream, each iteration yields one chunk (a Buffer by default, or a string if the stream encoding is set).
Here we read this script's own source file line-by-chunk. The loop body runs once per chunk and the loop naturally ends when the stream finishes:
import { createReadStream } from 'node:fs';
import { fileURLToPath } from 'node:url';
const self = fileURLToPath(import.meta.url);
const stream = createReadStream(self, { encoding: 'utf8' });
let chunks = 0;
let bytes = 0;
for await (const chunk of stream) {
chunks++;
bytes += chunk.length;
}
console.log('chunks:', chunks, 'chars:', bytes);Built-in Backpressure
The biggest win of for await...of is automatic backpressure. The loop requests the next chunk only when the previous iteration's body has finished awaiting. If your body does slow async work (a DB write, an HTTP call), the stream is paused until you are ready.
You get this for free — no manual stream.pause() / stream.resume() dance. This simulated example processes each chunk with a delay; the stream waits between iterations:
import { Readable } from 'node:stream';
import { setTimeout as sleep } from 'node:timers/promises';
const source = Readable.from(['a', 'b', 'c', 'd']);
for await (const item of source) {
// Slow consumer: stream is paused while we await
await sleep(50);
console.log('processed', item);
}
console.log('done');Readable.from(): Iterable to Stream
Readable.from() turns any iterable or async iterable (arrays, generators, async generators) into a proper Readable stream. This is the bridge from plain data or generator logic into the stream ecosystem (pipes, HTTP responses, file writes).
You can hand it a generator function result to lazily produce values:
import { Readable } from 'node:stream';
function* counter(n) {
for (let i = 1; i <= n; i++) {
yield `line ${i}\n`;
}
}
const stream = Readable.from(counter(3));
for await (const chunk of stream) {
process.stdout.write(chunk);
}Async Generators as Stream Sources
Readable.from() also accepts an async generator. This lets you produce stream data from asynchronous sources — paginated APIs, queued jobs, or timed events — while keeping clean sequential code.
The async generator below yields a record every short interval, and Readable.from() exposes it as a standard stream any Node consumer can read:
import { Readable } from 'node:stream';
import { setTimeout as sleep } from 'node:timers/promises';
async function* fetchPages() {
for (let page = 1; page <= 3; page++) {
await sleep(20); // simulate async fetch
yield { page, items: page * 2 };
}
}
const stream = Readable.from(fetchPages(), { objectMode: true });
for await (const record of stream) {
console.log('got page', record.page, 'items', record.items);
}Object Mode Streams
By default streams carry Buffer or string chunks. When you set objectMode: true, each chunk is an arbitrary JavaScript value — an object, a number, an array. This is essential when async-iterating over structured records rather than raw bytes.
Readable.from() auto-enables object mode when the source yields non-string/Buffer values, but being explicit keeps intent clear. Async iteration then yields your objects directly:
import { Readable } from 'node:stream';
const users = [
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Linus' },
{ id: 3, name: 'Grace' },
];
const stream = Readable.from(users, { objectMode: true });
for await (const user of stream) {
console.log(`#${user.id} -> ${user.name}`);
}Transforming via Async Generators
You do not always need a Transform stream class to map or filter stream data. You can wrap the source stream in an async generator that itself uses for await...of, yielding transformed values. This composes naturally and reads like synchronous code.
Here a generator parses NDJSON-style lines into objects and filters them, all while preserving backpressure:
import { Readable } from 'node:stream';
async function* onlyActive(source) {
for await (const record of source) {
if (record.active) {
yield record.name;
}
}
}
const input = Readable.from([
{ name: 'job-a', active: true },
{ name: 'job-b', active: false },
{ name: 'job-c', active: true },
], { objectMode: true });
for await (const name of onlyActive(input)) {
console.log('active:', name);
}Error Handling with try/catch
A major ergonomic gain: stream errors surface as thrown exceptions inside the loop, so a normal try/catch handles them. With event-based consumption you had to listen for 'error' separately and risk unhandled rejections.
If the stream emits 'error' mid-iteration, the awaited next() rejects and control jumps to catch:
import { Readable } from 'node:stream';
async function* faulty() {
yield 1;
yield 2;
throw new Error('source blew up');
}
const stream = Readable.from(faulty(), { objectMode: true });
try {
for await (const n of stream) {
console.log('value', n);
}
} catch (err) {
console.error('caught:', err.message);
}Early break Destroys the Stream
When you break, return, or throw out of a for await...of loop, Node calls the iterator's return() method, which destroys the underlying stream and releases its resources (file descriptors, sockets). This is a crucial cleanup guarantee.
So stopping after the first match does not leak the open file handle — the stream is torn down automatically:
import { Readable } from 'node:stream';
const stream = Readable.from(['alpha', 'beta', 'gamma', 'delta']);
for await (const word of stream) {
console.log('checking', word);
if (word === 'beta') {
console.log('found it, stopping');
break; // stream is destroyed on break
}
}
console.log('destroyed?', stream.destroyed);Consuming an HTTP Request Body
In a Node backend, an incoming HTTP request (req) is a Readable stream. Async iteration is a clean way to collect or process the request body without buffering everything manually.
This pattern accumulates chunks then parses JSON. Because it is server code (it binds a port and needs an HTTP client), it is illustrative rather than self-running here:
import { createServer } from 'node:http';
const server = createServer(async (req, res) => {
try {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks).toString('utf8');
const data = JSON.parse(body || '{}');
res.end(JSON.stringify({ received: data }));
} catch (err) {
res.statusCode = 400;
res.end('bad request');
}
});
server.listen(3000);Iterator Helpers and stream.compose()
Modern Node adds higher-order helpers directly on Readable streams that consume the async iterator under the hood: stream.map(), stream.filter(), stream.toArray(), stream.take(), and more. They keep backpressure while letting you avoid hand-written loops.
You can chain them fluently. toArray() drains the stream and resolves to a plain array:
.map(fn)— transform each chunk (supports async fn + concurrency).filter(fn)— keep matching chunks.toArray()— collect everything into an array
import { Readable } from 'node:stream';
const result = await Readable.from([1, 2, 3, 4, 5])
.filter((n) => n % 2 === 1)
.map((n) => n * 10)
.toArray();
console.log(result); // [10, 30, 50]Quick Check
You consume a Readable file stream with for await...of and break out of the loop after finding the first matching line. What happens to the underlying file resource?
Recap
You learned to consume and produce Node streams with async iteration:
- for await...of reads a Readable one chunk at a time with built-in backpressure and try/catch error handling.
- Readable.from() converts arrays, generators, and async generators into streams; use
objectModefor structured records. - Wrap a source in an async generator to map/filter while preserving backpressure — no Transform class needed.
- break/return/throw destroys the stream and frees resources automatically.
- Helpers like
.map(),.filter(), and.toArray()offer a fluent alternative to manual loops.
Reach for async iteration whenever you want sequential, readable, leak-safe stream consumption in your backend.
常见问题解答
「异步迭代器与通过 for-await-of 遍历流」课时是免费的吗?
是的 — 「异步迭代器与通过 for-await-of 遍历流」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Node.js Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 Node.js Backend Development Bootcamp 课程共包含 4 节课。
「异步迭代器与通过 for-await-of 遍历流」这节课中我会学到什么?
使用异步迭代便捷地消费可读流,并在流与异步生成器之间进行转换。 你通过在浏览器中直接运行的动手代码来练习 Node.js Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Node.js Backend Development Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Node.js Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「异步迭代器与通过 for-await-of 遍历流」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Node.js Backend Development Bootcamp 课中编写并运行代码吗?
能。每节 Node.js Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 可读、可写、双工与转换流内部机制
- 使用 _transform 和 _flush 实现自定义转换流
- 背压、pipe() 与 pipeline() 工具
- 异步迭代器与通过 for-await-of 遍历流