mrkeyoor.com_
Sat 08 Aug 20:59 UTC
npmUtilsupdated 08 Aug 2026

walkdir

walkdir is a zero-dependency Node.js directory walker with three interfaces: an asynchronous EventEmitter, a promise-returning helper, and a synchronous function. It emits separate events for files, directories, links, empty directories, sockets, FIFOs, and device nodes, and it can filter entries, stop, pause, ignore subtrees, follow symbolic links, or collect paths and fs.Stats objects. It predates modern async iterators and glob libraries, but remains useful when event-level control matters.

Verdict

walkdir still offers an unusually flexible emitter for legacy CommonJS scripts, especially dynamic ignore and node-type events. For new code, fdir or readdirp offers a more current async shape, types, and maintenance story.

API stability3/5The callback, emitter, sync, and promise entry points have remained unchanged because 0.4.1 has not been republished since 2019. The behavior is easy to preserve in existing code, but the package is still below 1.0 and several contracts are only implicit in source, including targetdirectory treatment, maxdepth events, promise rejection on child fail, and event ordering.
Docs3/5The README covers all three interfaces, options, node-type events, failure events, pause, resume, end, and dynamic ignore with examples. It also warns about inode tracking on Windows and hard links. It does not document the emitted depth argument, maxdepth event, asynchronous filter rejection gap, root-event distinction, nondeterministic order, or the fact that pause queues events without stopping reads.
Maintenance2/5The repository is not archived and GitHub reports 7 open issues and pull requests in its combined counter, but npm 0.4.1 was published in July 2019 and the last code push was in December 2022. The code still declares Node 6 support and uses older callback and EventEmitter patterns, with no recent release proving behavior on current Node filesystems.
Ecosystem3/5walkdir recorded 4,186,011 downloads for the measured week, has 129 GitHub stars, and its simple CommonJS API is embedded in many older dependency trees. It has no plugin system, built-in types, ESM build, glob language, or framework integrations. Its custom fs option is a useful extension point, but callers must provide a fairly complete Node-style filesystem surface.

Use it if

  • Existing CommonJS code already depends on walkdir's file, directory, link, fail, and empty events
  • You need to ignore a directory dynamically from an event callback or pause and resume event delivery
  • A one-off Node script needs both sync and async tree walking with no runtime dependencies
  • You need to inject an fs-compatible implementation and can satisfy the package's stat, lstat, readdir, and readlink contract
Skip it if

Setup reality

npm install walkdir is the only setup step. There are no dependencies, native builds, peers, credentials, or config files, and Node 6 or newer is declared. The runtime is CommonJS, so use require('walkdir'); TypeScript users need their own declaration or the separately maintained @types package if suitable. Choose the interface carefully. walk(path) starts immediately and returns an EventEmitter, so attach listeners in the same tick. The initial directory emits targetdirectory, not the ordinary path and directory events. Child read or stat failures emit fail; only inability to read the requested target also becomes error. walk.async converts any fail into a rejected promise, so a single permission-denied descendant rejects the whole call even though the underlying walk can still have in-flight work. By default the package uses lstat, emits link entries, and does not follow them. follow_symlinks can create cycles; inode tracking is enabled, but the README warns that inode identity is unreliable on some filesystems and hard-link situations. Combine max_depth with symlink following, and listen for the source's maxdepth event if truncation matters. no_recurse still reads the root once, producing one-level results. no_return prevents building a potentially huge list. pause only queues emitted events; it does not pause filesystem reads, and end suppresses further handling without cancelling in-flight fs operations. The filter callback must return an array or a resolving promise of filenames, not full paths.

Patterns

Visit every child path asynchronouslywalk-with-callback

const walk = require('walkdir');

walk('./content', function (pathname, stat) {
  console.log(pathname, stat.isFile());
});

The callback is attached to path events. The starting directory itself emits targetdirectory instead of path.

Handle files and directories separatelylisten-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);
});

Attach listeners immediately because the walk starts before walk() returns. Async event order follows filesystem completion, not sorted path order.

Collect paths with the promise APIawait-path-list

const paths = await walk.async('./content');
console.log(paths);

Any descendant fail event rejects walk.async. The returned path order is not deterministic, so sort it when output order matters.

Collect a path-to-stats objectawait-path-stats

const entries = await walk.async('./content', {
  return_object: true,
});

for (const [pathname, stat] of Object.entries(entries)) {
  console.log(pathname, stat.size);
}

The object retains an fs.Stats instance for every emitted path and can consume substantial memory on a large tree.

Return paths synchronouslywalk-synchronously

const paths = walk.sync('./fixtures');
for (const pathname of paths) {
  console.log(pathname);
}

Synchronous traversal blocks the event loop. Keep it to startup code, tests, and small command-line jobs.

Avoid retaining a huge result liststream-without-collecting

const emitter = walk('./large-tree', { no_return: true });

emitter.on('file', (pathname) => indexFile(pathname));
emitter.on('end', () => console.log('done'));

no_return matters for sync and promise collection. The EventEmitter interface already lets you process entries incrementally.

Filter names before descendingfilter-directory-entries

const emitter = walk('./project', {
  filter(directory, files) {
    return files.filter((name) => name !== 'node_modules' && !name.startsWith('.'));
  },
});

Return entry names, not joined paths. If an async filter rejects, version 0.4.1 does not handle that rejection cleanly.

Skip a subtree after inspecting its pathignore-directory-dynamically

const path = require('path');

walk('./project', function (pathname, stat) {
  if (stat.isDirectory() && path.basename(pathname) === '.git') {
    this.ignore(pathname);
  }
});

The callback's this value is the emitter. Call ignore when the directory path event arrives, before its queued readdir is processed.

Cap traversal depthlimit-walk-depth

const emitter = walk('./project', { max_depth: 3 });

emitter.on('maxdepth', (pathname, stat, depth) => {
  console.warn('not descending', depth, pathname);
});

The source emits maxdepth when the cap is reached; it does not emit error despite wording in the options section of the README.

Continue after an unreadable childhandle-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 is for descendant stat or read failures; error is reserved for failure of the requested starting path.

Follow links with a depth safety capfollow-symbolic-links

const emitter = walk('./tree', {
  follow_symlinks: true,
  max_depth: 20,
  track_inodes: true,
});

emitter.on('link', (pathname) => console.log('link', pathname));

Symlink loops are possible. Keep inode tracking enabled and set max_depth, especially on filesystems where inode identity is unreliable.

Stop accepting more resultscancel-active-walk

const emitter = walk('./tree');
let files = 0;

emitter.on('file', () => {
  if (++files >= 100) emitter.end();
});

end suppresses later processing but does not cancel fs calls already in flight. There is no AbortSignal integration.

Alternatives

PackageRegistryPick it when
fdirnpmYou want a fast maintained crawler with promise, callback, glob, and builder-style options
readdirpnpmYou want a maintained Node directory stream with filters and async-iterator consumption
fast-globnpmYour real requirement is matching glob patterns with ignore rules rather than observing filesystem node types
klawnpmYou want a simple readable-object stream of paths and stats in older CommonJS code