walkdir review
walkdir 0.4.1 traverses a Node filesystem tree through an EventEmitter, a promise helper, or a synchronous call. Its events distinguish files, directories, links, empty folders, sockets, FIFOs, and device nodes. A running emitter can ignore a subtree, queue delivery with pause(), or stop reporting through end(); options cover depth, symlink following, result collection, and a replacement fs object. Our package check found one 196 KB CommonJS package with no dependencies or types. It predates async iteration and is best understood as a legacy event walker rather than a glob engine.
walkdir 0.4.1 installed as one 1 MB package in 0.7 seconds with 0 audit findings, but our browser build failed and no types were bundled. Keep it for legacy event-driven walkers; choose fdir or readdirp for a maintained async API in new Node code.
We installed it
| Install | ✓ · 0.7s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does walkdir install cleanly?
Yes. In a fresh container with an empty cache, npm install walkdir finished in 0.7s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
Can walkdir run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does walkdir work with both ESM and CommonJS?
Yes. Both import 'walkdir' and require('walkdir') worked in Node 22 in our run. The package is published as CommonJS.
Does walkdir include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
walkdir or fdir: which should you use?
fdir: Use it for a maintained high-speed crawler with promise, callback, glob, and builder-style configuration. walkdir 0.4.1 installed as one 1 MB package in 0.7 seconds with 0 audit findings, but our browser build failed and no types were bundled.
When should you not use walkdir?
Release activity or a 1.x compatibility promise is required. npm published 0.4.1 in July 2019, and GitHub shows the last push in December 2022.
Use it if
- Existing CommonJS code already consumes its file, directory, link, empty, fail, and end events.
- Traversal decisions must ignore a directory from inside an event callback or temporarily queue event delivery.
- A small Node script needs synchronous and asynchronous walking without adding runtime dependencies.
- A custom filesystem implementation can provide Node-style stat, lstat, readdir, and readlink methods.
- Release activity or a 1.x compatibility promise is required. npm published 0.4.1 in July 2019, and GitHub shows the last push in December 2022.
- The API must include ESM exports, bundled types, async iteration, or AbortSignal cancellation. This CommonJS release ships none of them.
- The task is glob matching with ignore files and stable ordering. Filters receive directory entries, while async filesystem completion determines output order.
- Permission-denied descendants are expected in a promise workflow. walk.async rejects on any fail event, whereas the emitter can report a failed child and continue.
- Filter promises can reject. The source attaches a success handler without a rejection path, risking an unhandled rejection and unfinished internal work.
Setup reality
We installed walkdir 0.4.1 in 0.7 seconds in a fresh Node 22 Bookworm container. It left 1 package and 1 MB on disk, with 0 known vulnerabilities from npm audit. The package has no direct or peer dependencies, is 196 KB unpacked, carries an MIT license, and declares Node >=6. It is CommonJS without an exports map. require() and ESM import succeeded in our checks, but no TypeScript declarations were present. Our esbuild browser build failed, as expected for Node filesystem code.
walk(path) begins immediately and returns an EventEmitter, so register listeners in the same turn. The starting directory emits targetdirectory instead of normal path and directory events. Failure to read the target emits error; a child stat or read failure emits fail. walk.async turns every fail into promise rejection, even though filesystem operations already in flight may continue. Use the emitter when inaccessible descendants should be reported without rejecting the entire scan.
The default uses lstat and reports symlinks without following them. follow_symlinks can create cycles. Inode tracking attempts to prevent that, but the README warns about inode identity on Windows and with hard links. Pair following with max_depth when termination matters. no_recurse still reads one level. no_return avoids accumulating a large path array or object when events are the real output.
pause() queues events rather than pausing filesystem reads, and end() suppresses later handling without cancelling outstanding fs calls. Async completion order is not deterministic. A filter receives the current directory and an array of entry names, then must return an array or a promise that resolves to one. Rejected filter promises lack a source-level rejection handler, so keep filters synchronous or catch and convert their failures before returning.
Patterns
Receive each discovered path walk-with-callback
const walk = require('walkdir');
walk('./content', function (pathname, stat) {
console.log(pathname, stat.isFile());
});The callback receives path events only. walkdir reports the root through targetdirectory, so it is absent from this callback stream.
Listen by filesystem node type listen-by-node-type
const emitter = walk('./content');
emitter.on('file', (pathname, stat, depth) => {
console.log('file', depth, pathname);
});
emitter.on('directory', (pathname, stat, depth) => {
console.log('directory', depth, pathname);
});Traversal begins at once, so listeners belong directly after walk(). Event order follows asynchronous filesystem completion and is not sorted.
Await a list of paths await-path-list
const paths = await walk.async('./content');
console.log(paths);One child fail rejects walk.async. Sort the resulting array yourself when callers depend on stable order.
Return stats indexed by path await-path-stats
const entries = await walk.async('./content', {
return_object: true,
});
for (const [pathname, stat] of Object.entries(entries)) {
console.log(pathname, stat.size);
}return_object retains an fs.Stats value for every result. Large directory trees can make that object expensive to keep.
Run a blocking directory walk walk-synchronously
const paths = walk.sync('./fixtures');
for (const pathname of paths) {
console.log(pathname);
}walk.sync() blocks Node until traversal finishes. Restrict it to small CLI tasks, tests, or controlled startup work.
Process events without collection stream-without-collecting
const emitter = walk('./large-tree', { no_return: true });
emitter.on('file', (pathname) => indexFile(pathname));
emitter.on('end', () => console.log('done'));no_return prevents an accumulated array or object. The emitter still reports entries as they arrive.
Filter one directory's entries filter-directory-entries
const emitter = walk('./project', {
filter(directory, files) {
return files.filter((name) => name !== 'node_modules' && !name.startsWith('.'));
},
});Return names from the supplied files array rather than full paths. Version 0.4.1 lacks a clean rejection path for asynchronous filters.
Ignore a matching subtree ignore-directory-dynamically
const path = require('path');
walk('./project', function (pathname, stat) {
if (stat.isDirectory() && path.basename(pathname) === '.git') {
this.ignore(pathname);
}
});The callback is bound to the emitter. Invoke ignore() as soon as the directory event arrives, before its pending readdir work proceeds.
Stop descent at a depth limit-walk-depth
const emitter = walk('./project', { max_depth: 3 });
emitter.on('maxdepth', (pathname, stat, depth) => {
console.warn('not descending', depth, pathname);
});Source behavior emits maxdepth at the boundary. It does not raise error, despite the option text saying the cap emits an error.
Report child permission failures handle-permission-failures
const emitter = walk('/srv/data');
emitter.on('fail', (pathname, err) => {
console.warn('skipped', pathname, err?.code);
});
emitter.on('error', (err) => {
console.error('target failed', err);
});fail identifies unreadable descendants and can be handled while walking continues. error means the requested root itself could not be read.
Follow symlinks with a bound follow-symbolic-links
const emitter = walk('./tree', {
follow_symlinks: true,
max_depth: 20,
track_inodes: true,
});
emitter.on('link', (pathname) => console.log('link', pathname));Link cycles can recurse indefinitely. Keep inode tracking on and add max_depth for filesystems where inode identity may collide.
End a running traversal cancel-active-walk
const emitter = walk('./tree');
let files = 0;
emitter.on('file', () => {
if (++files >= 100) emitter.end();
});end() stops later result handling, while filesystem calls already started still run. The API has no AbortSignal hook.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fdir | npm | Use it for a maintained high-speed crawler with promise, callback, glob, and builder-style configuration. |
| readdirp | npm | Use it for a maintained directory stream with filters and for-await consumption. |
| fast-glob | npm | Use it when patterns and ignore rules matter more than events for each filesystem node type. |
| klaw | npm | Use it for a readable object stream of paths and stats in older CommonJS code. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

