Node.js Async Iterators Tutorial
Master async iterators in Node.js for streaming files, paginated APIs, and backpressure-aware data processing.
What you'll learn
- ✓What async iterators are
- ✓How for await works
- ✓Building custom async generators
- ✓Backpressure for free
- ✓When not to use them
Prerequisites
- •Comfortable with async/await in JavaScript
What and Why
A regular iterator hands you the next value synchronously. An async iterator hands you a promise that resolves to the next value. That subtle shift turns out to be one of the most useful features in modern Node.js. It lets you write code that reads from a file, a network stream, or a paginated API as if it were a simple for loop.
Why does that matter? Because most Node.js work is IO. Async iterators let you express “give me one chunk at a time, wait for it, process it, then ask for the next” without callbacks or stream events. Backpressure is built in: you only ask for the next chunk when you are ready.
Mental Model
An iterable has a method that returns an iterator. An iterator has a next() method that returns { value, done }. An async iterator does the same, except next() returns a promise. JavaScript provides for await (const x of source) to consume them.
Anything that implements Symbol.asyncIterator is fair game. Node streams, file handles, fetch response bodies, and your own generators all qualify. That single protocol unifies very different sources behind one syntax.
Hands-on Example
Read a large file line by line without loading it into memory.
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function countErrors(path) {
const stream = createReadStream(path, { encoding: 'utf8' });
const lines = createInterface({ input: stream, crlfDelay: Infinity });
let errors = 0;
for await (const line of lines) {
if (line.includes('ERROR')) errors++;
}
return errors;
}
console.log(await countErrors('./app.log'));
Or build your own with an async generator. A pager that yields one page of API results at a time.
async function* paginate(url, pageSize = 50) {
let cursor = null;
while (true) {
const u = new URL(url);
u.searchParams.set('limit', pageSize);
if (cursor) u.searchParams.set('cursor', cursor);
const res = await fetch(u);
const { items, next } = await res.json();
for (const item of items) yield item;
if (!next) return;
cursor = next;
}
}
for await (const item of paginate('https://api.example.com/orders')) {
await processOrder(item);
}
Consumer asks -> next() -> Promise
|
v
Source fetches/reads
|
v
Resolve { value, done }
|
v
Consumer processes, loops Notice how processOrder can await without disturbing the producer. The next page only loads after the current one is fully consumed. Backpressure is free.
Common Pitfalls
- Mixing parallelism with
for await. The loop is strictly sequential. To process N items concurrently, batch them and usePromise.allinside the loop. - Forgetting cleanup. If you break out of a
for awaitearly, the iterator is automatically closed via itsreturn()method. Make sure custom generators handlereturn(atry/finallyworks). - Treating iterators as arrays. They cannot be iterated twice. If you need to re-read, collect into an array first.
- Reading streams the wrong way.
for awaiton a stream consumes it. Calling it again on the same stream yields nothing. - Long generator chains. They are elegant but stack traces become harder to read. Add labels or comments when the chain grows.
Practical Tips
- Use
node:stream’sReadable.from(asyncIterable)to convert any async iterable into a stream, and vice versa withfor await (const chunk of readable). - For controlled concurrency, write a helper that pulls N items at a time and awaits them with
Promise.all. - Wrap external APIs in async generators to hide pagination behind a familiar
for awaitloop. - For very high throughput, prefer typed buffers and avoid string concatenation inside loops.
- Use
AbortSignalto cancel long iterations cleanly; pass it into your generator and check it between yields.
async function* withCancel(source, signal) {
for await (const item of source) {
if (signal.aborted) return;
yield item;
}
}
Wrap-up
Async iterators unify how Node.js consumes streaming data. Files, network responses, paginated APIs, and your own generators all speak the same protocol, and for await lets you process them with the simplicity of a synchronous loop. Add an AbortSignal for cancellation and a small helper for controlled concurrency, and you have a clean pattern for almost every IO-heavy task you will write.
Related articles
- JavaScript The JavaScript Event Loop, Explained Clearly
A deep but practical look at the JavaScript event loop: call stack, microtasks, macrotasks, and how async code actually runs in the browser and Node.
- JavaScript JavaScript Generators and Iterators
A practical guide to JavaScript iterators and generator functions: the protocols, lazy sequences, async generators, and where they shine in real code.
- Node.js Node fs/promises and Streams Tutorial
Read and write files in modern Node using fs/promises, and learn when to switch to streams for memory-friendly processing of large data.
- Node.js Debugging Node.js Memory Leaks
Find and fix memory leaks in Node.js using heap snapshots, sampling, and a few reliable patterns to avoid leaks.