异步迭代器在 Node.js 18 里是处理流和自定义遍历的一种通用机制,但很多报错其实出在协议判断上。如果你用 for await 时遇到 TypeError,先不要改业务代码,先看目标对象有没有实现 Symbol.asyncIterator。
先确认对象是否真的支持异步迭代协议
一个对象是否为异步迭代器,要看它是否实现了 Symbol.asyncIterator 方法。这个方法返回一个异步迭代器,其 next() 方法返回 Promise。在 Node.js 18 中,for await...of 循环会自动调用这个 Symbol.asyncIterator 方法,如果对象不具备该方法则抛出 TypeError。判断时可以直接用 typeof obj[Symbol.asyncIterator] === 'function' 确认,或者先尝试 for await 看是否报错。
检查时可以写一个工具函数,返回布尔值。如果确认不支持,就要考虑替代方案:内置的 Readable 流可以直接用,普通数组可以用 Readable.from() 包装。
async function isAsyncIterable(obj) {
return typeof obj?.[Symbol.asyncIterator] === 'function';
}
for await 消费流时,注意数据块的边界
在 Node.js 18 中,异步迭代器最常见的消费方式是 for await...of。比如对 fs.createReadStream 返回的 Readable 流,可以用 for await (const chunk of stream) 读取数据块。需要注意 for await 要求目标对象必须实现异步迭代协议,普通数组或可迭代对象不能直接使用,需要先使用 Readable.from() 将数组包装成异步可迭代的流。
这里的“数据块”不是业务上的记录,而是流内部缓冲区的片段。读取文件时,一个 chunk 可能是几 KB 到几十 KB,不要假设一次读取正好是一行 JSON 或一段配置。如果需要按完整行处理,可以先累积到 Buffer 里,再按分隔符切分。下面是一个通用的累积写法:
let fullBuffer = Buffer.alloc(0);
for await (const chunk of stream) {
fullBuffer = Buffer.concat([fullBuffer, chunk]);
}
错误处理要提前设计,try/catch 与 finally 配合
无论用内置流还是自定义异步迭代器,错误处理都要提前想清楚。for await...of 内部会等待每个 next() 返回的 Promise 落定。如果某个 Promise 被 reject,循环会立即退出并抛出异常。捕获方式与同步循环基本一致,用 try/catch 包裹整个循环即可。但要注意两点:第一,已经消费过的数据不会回滚,异常发生时无法回到上一次成功的位置;第二,如果迭代器实现了 return() 方法,异常或 break 会触发它,但如果你没有实现,清理逻辑就不会执行。
try {
for await (const chunk of stream) {
process(chunk);
}
} catch (err) {
console.error('读取出错', err);
} finally {
await stream.destroy();
}
自定义异步迭代器,next() 和 return() 都要按协议实现
自定义异步迭代器时,最核心的是 next() 必须返回 Promise。虽然在某些环境下返回带 then 的对象也能被识别,但协议要求就是 Promise,不要依赖这种兼容行为。另一个经常被忽略的是 return() 方法。这个方法用于提前退出时的清理,比如 for await 中执行了 break 或抛出异常,如果迭代器没有实现 return(),底层资源可能不会被释放。对文件句柄或数据库连接来说,这就是泄漏。
可以这样实现一个简单的异步迭代器:
function createAsyncIterable() {
let i = 0;
return {
[Symbol.asyncIterator]() {
return this;
},
async next() {
if (i < 3) return { value: i++, done: false };
return { done: true };
},
async return() {
console.log('清理被调用');
return { done: true };
}
};
}
流状态和并发,两个容易忽略的边界条件
使用流作为异步迭代器时,需要先确认流没有进入 flowing 模式。如果之前挂过 data 事件或用过 pipe(),流可能已经不在异步迭代状态。检查 stream.readableFlowing 属性,如果为 true,先调用 stream.pause()。另外,不要在同一个流上启动多个 for await 循环,多个循环会互相争抢数据块,导致数据读取不完整。如果必须并发,用 pipeline 或 PassThrough 做分流。
if (stream.readableFlowing === true) {
stream.pause();
}
性能上做取舍,先观察再优化
在 Node.js 18 中,异步迭代器由于每次 next() 都会产生 Promise 微任务,高吞吐场景下可能比事件回调慢。但优势在于代码可读性和暂停控制。如果希望提高性能,可以在循环内使用 Buffer.concat 积累块,而不是逐块处理。同时,注意 destroy 流时要捕获 'error' 事件,否则未处理的错误可能导致进程崩溃。这里没有具体 benchmark 数据,实际差异取决于场景。
所以不要一上来就追求性能,先确认业务瓶颈是否在于逐块处理。如果只是小文件或低并发,for await 的便利性远大于性能差异。如果要优化,优先考虑在循环内累积 Buffer,减少处理次数,同时为 stream 绑定 error 监听。销毁时用下面的模式可以避免未处理异常:
stream.on('error', (err) => {
console.error('流错误', err);
});
await stream.destroy();