mrkeyoor.com_
Wed 05 Aug 19:58 UTC
npmUtilsupdated 05 Aug 2026

chokidar

Chokidar watches files and directories for changes and reports them as clean add, change, and unlink events instead of the raw, duplicated, and platform-inconsistent output of Node's fs.watch. It handles the mess for you: macOS events get real filenames, editors that write via temp-file rename produce one change event instead of unlink plus add, chunked writes can be held until the file stops growing, and recursion works everywhere with an optional depth limit. Built for the Brunch build tool in 2012, it became the watching layer under a huge share of dev servers and CLI tools.

Verdict

The default answer for file watching in Node and still the most battle-tested set of platform workarounds available; v4 and v5 made it smaller and dependency-light. Plan around the two breaking cliffs (globs gone in v4, ESM-only in v5) before you upgrade rather than after.

API stability3/5The watch/on/close API is old and steady, but v4 removed glob support, a feature most v3 users relied on, and v5 went ESM-only with a Node 20.19 floor; two majors in about a year both required real migration.
Docs4/5The README documents every option with defaults and honest performance caveats, plus a troubleshooting section for EMFILE and ENOSPC; there is no separate docs site or typed cookbook beyond it.
Maintenance4/5Pushed July 2026 with 49 open issues and PRs; Paul Miller maintains it actively and v5 shipped Nov 2025, though it is effectively a one-maintainer project.
Ecosystem5/5Around 200 million weekly downloads and, per the README, used in roughly 30 million repositories; it is the watching layer inside a large share of dev tooling.

Use it if

  • You are building anything with a watch mode (dev server, test runner, build tool) and need events that mean what they say across macOS, Linux, and Windows
  • You need to survive editor atomic writes and large chunked writes: the atomic and awaitWriteFinish options exist precisely for those cases
  • You watch network drives or containers where inotify does not work; usePolling switches the backend without changing your code
  • You want a small dependency: v4+ has exactly one dependency (readdirp) and no native compile step since fsevents was dropped
Skip it if

Setup reality

npm install chokidar and you are done: one dependency, TypeScript types bundled, no postinstall or native builds since the fsevents days ended with v4. The friction is version and platform shaped. v5 is ESM-only and needs Node 20.19+, so CJS projects stay on v4. If you came from v3, every glob you passed to watch() or unwatch() silently means something else now and must become an ignored function. On Linux, big trees exhaust inotify watch limits (ENOSPC) until you raise fs.inotify.max_user_watches, and EMFILE errors on huge projects may need graceful-fs or polling.

Patterns

Watch a directory and log every eventwatch-directory

import chokidar from 'chokidar';

chokidar.watch('.').on('all', (event, path) => {
  console.log(event, path);
});

v5 is ESM-only; require('chokidar') throws in CommonJS, use v4 there. Watching '.' recurses fully, so scope the path or set depth to avoid watching node_modules.

Listen for add, change, and unlink separatelyspecific-events

import chokidar from 'chokidar';

const watcher = chokidar.watch('./src', { persistent: true });

watcher
  .on('add', path => console.log(`added ${path}`))
  .on('change', path => console.log(`changed ${path}`))
  .on('unlink', path => console.log(`removed ${path}`))
  .on('addDir', path => console.log(`dir added ${path}`))
  .on('unlinkDir', path => console.log(`dir removed ${path}`))
  .on('error', err => console.error('watcher error', err));

Always attach an error handler; without one, an EMFILE or permission error crashes the process.

Watch only certain file types (globs are gone)filter-by-extension

import chokidar from 'chokidar';

// v3: chokidar.watch('**/*.js')  <- no longer works
chokidar.watch('.', {
  ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'),
});

Since v4, watch() takes paths only. The ignored function is called twice per path, once without stats and once with, so guard with stats?.isFile() before rejecting.

Ignore node_modules and dotfilesignore-directories

import chokidar from 'chokidar';

chokidar.watch('./project', {
  ignored: (path) => path.includes('node_modules') || /(^|[\/\\])\../.test(path),
  ignoreInitial: true,
});

ignoreInitial: true suppresses the flood of add events for files that already exist when the watcher starts; without it you get one add per existing file before ready.

Act only after the initial scan finisheswait-for-ready

import chokidar from 'chokidar';

const watcher = chokidar.watch('./data');

watcher.on('ready', () => {
  console.log('initial scan done');
  watcher.on('add', path => console.log('new file:', path));
});

Attaching add handlers inside ready is the other way to skip startup noise if you still want the initial file list via getWatched().

Hold events until large files finish writingawait-write-finish

import chokidar from 'chokidar';

chokidar.watch('./uploads', {
  awaitWriteFinish: {
    stabilityThreshold: 2000,
    pollInterval: 100,
  },
}).on('add', path => processUpload(path));

Without this, add fires when the file first appears, often before the writer is done. The threshold means every event is delayed at least that long, so keep it as low as your producers allow.

Use polling for network or virtual filesystemspolling-network-drives

import chokidar from 'chokidar';

chokidar.watch('/mnt/share', {
  usePolling: true,
  interval: 100,
  binaryInterval: 300,
});

fs.watch events usually do not propagate over NFS/SMB or some Docker mounts; polling works everywhere but costs CPU proportional to file count. CHOKIDAR_USEPOLLING=1 flips it via environment.

Add and remove watched paths at runtimeadd-unwatch-paths

import chokidar from 'chokidar';

const watcher = chokidar.watch('./a');
watcher.add(['./b', './c']);
await watcher.unwatch('./a');
console.log(watcher.getWatched());

unwatch is async in recent versions; getWatched() returns an object keyed by directory with arrays of contained names, useful for debugging what is actually being watched.

Shut down a watcher cleanlyclose-watcher

import chokidar from 'chokidar';

const watcher = chokidar.watch('./src');
// later, e.g. on SIGINT:
await watcher.close();
console.log('watcher closed');

close() is async; if you forget to await it in tests, file handles leak between cases and you get flaky cross-test events.

Get event paths relative to a base directoryrelative-paths-cwd

import chokidar from 'chokidar';

chokidar.watch('src', { cwd: 'src' })
  .on('change', path => {
    // path is like 'components/App.js', not 'src/components/App.js'
    console.log(path);
  });

Without cwd, emitted paths mirror however you specified the watch path (relative or absolute), which regularly surprises people building path maps.

Use fs.Stats delivered with eventsstats-on-events

import chokidar from 'chokidar';

chokidar.watch('./logs', { alwaysStat: true })
  .on('change', (path, stats) => {
    if (stats) console.log(`${path} is now ${stats.size} bytes`);
  });

add, addDir, and change may include stats for free when the backend provides them; alwaysStat: true guarantees it at the cost of an extra stat call per event.

Limit recursion depthlimit-depth

import chokidar from 'chokidar';

chokidar.watch('./project', {
  depth: 1, // watch project/ and one level of subdirs only
});

Each watched directory consumes an OS watch handle; on Linux, deep trees hit fs.inotify.max_user_watches (ENOSPC) unless you raise the sysctl or bound depth.

Alternatives

PackageRegistryPick it when
@parcel/watchernpmYou want native-code watching performance and snapshot support, and can accept a compiled dependency.
node-watchnpmYou want a tiny wrapper over fs.watch with filtering and recursion and can live with fewer edge-case fixes.
watchpacknpmYou are in webpack's orbit and want the watcher webpack itself uses, with its aggregation semantics.