Generator functions pause at each yield and resume when the caller requests the next value. Nothing runs until you consume the iterator — with .next(), a for...of loop, or yield*. Each value is produced on demand, which means you can walk a huge directory tree without holding all the paths in memory at once.
Directory traversal is a natural fit. The generator recurses into subdirectories with yield* and yields only the files matching a given extension filter:
import { readdir } from 'node:fs/promises';
import { extname, resolve } from 'node:path';
async function* walkDir(dir: string): AsyncGenerator<string> {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const name = resolve(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(name);
} else if (filterFile(entry.name)) {
yield name;
}
}
}
const filterFile = (file: string): boolean => {
return ['.css', '.js', '.html', '.xml', '.cjs', '.mjs', '.svg', '.txt'].some(
(ext) => extname(file) === ext,
);
};Because it’s async, the event loop stays unblocked. You can also wrap it in try-catch, break early with return, or pipe results directly into processing logic.
For a deeper dive, the MDN docs on iterators and generators are worth reading.