fs.watch()

Watches a file or directory and invokes a listener whenever the operating system reports a change.

Signature
fs.watch(filename[, options][, listener])
  • Added in Node.js v0.5.10

Parameters

ParameterTypeDescription
$filenamestring | Buffer | URLThe file or directory to watch. A URL must use the file: protocol.
$optionsoptionalstring | ObjectWatch options. If a string is passed it is taken as the encoding.
$options.persistentoptionalboolean= trueWhether the process should keep running as long as the file is being watched.
$options.recursiveoptionalboolean= falseWatch all subdirectories rather than only the named one. Applies to directories, on supported platforms only.
$options.encodingoptionalstring= 'utf8'Character encoding for the filename passed to the listener. Use 'buffer' to receive a Buffer.
$options.signaloptionalAbortSignalAborting the corresponding controller closes the returned FSWatcher.
$options.throwIfNoEntryoptionalboolean= trueWhether an exception is thrown when the path does not exist.
$options.ignoreoptionalstring | RegExp | Function | Array= undefinedPattern(s) to ignore. Strings are glob patterns, RegExps are tested against the filename, and functions receive the filename and return true to ignore.
$listeneroptionalFunction | undefined= undefinedCalled as listener(eventType, filename). eventType is 'rename' or 'change'; filename may be null on some platforms.

Return value

fs.FSWatcherAn fs.FSWatcher, an EventEmitter emitting 'change' (with eventType and filename), 'error' and 'close'. It keeps the event loop alive until close() is called or the signal is aborted.

Errors

Throws ENOENT synchronously when filename does not exist (unless throwIfNoEntry is false, available from v26.1.0), throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM for recursive: true on platforms that do not support it, and emits 'error' on the returned FSWatcher when the watch is lost after it has started.

Changelog

  • PHP v26.1.0Added the throwIfNoEntry option, allowing fs.watch() on a missing path to return without throwing.
  • PHP v19.1.0Added recursive support for Linux, AIX and IBMi. Before this, recursive: true worked only on macOS and Windows.
  • PHP v15.9.0, v14.17.0Added support for closing the watcher with an AbortSignal.
  • PHP v15.9.0, v14.18.0fsPromises.watch() was added, exposing the same mechanism as an async iterator.
  • PHP v7.6.0The filename parameter can be a WHATWG URL object using the file: protocol.

fs.watch() hooks into the platform's native change-notification mechanism — inotify on Linux, kqueue and FSEvents on macOS, ReadDirectoryChangesW on Windows — and returns an fs.FSWatcher emitting 'change' events. It is far cheaper than fs.watchFile(), which polls with repeated stat() calls, so it is the right default for dev servers, config hot-reload and asset rebuild triggers.

It is also, as the official docs state plainly, not consistent across platforms.

What to expect

  • eventType is only ever 'rename' or 'change'. 'rename' covers creation and deletion as well as actual renames, and does not tell you which happened — you must stat() to find out.
  • One logical edit usually fires several events, because editors write a temp file, rename it into place, then update metadata. Debounce before doing real work.
  • filename may be null on some platforms, and when present it is relative to the watched directory, not a full path.
  • recursive works on macOS and Windows, and on Linux, AIX and IBMi only since v19.1.0.
  • Watching a single file that gets replaced by rename leaves you watching the old inode and silently deaf. Watch the containing directory instead.
  • The watcher keeps the event loop alive; call close(), pass a signal, or set persistent: false.

Example

Example · javascript
import fs from 'node:fs';
import fsp from 'node:fs/promises';

await fsp.mkdir('watched', { recursive: true });

const controller = new AbortController();

const watcher = fs.watch('watched', { signal: controller.signal }, (eventType, filename) => {
  // eventType is only ever 'rename' or 'change'.
  // 'rename' also covers creation and deletion.
  console.log(`${eventType}: ${filename}`);
});

watcher.on('error', (err) => console.error('watch failed:', err.code));
watcher.on('close', async () => {
  console.log('watcher closed');
  await fsp.rm('watched', { recursive: true, force: true });
});

// Changes on disk reach the listener asynchronously, via the OS.
await fsp.writeFile('watched/notes.txt', 'hello\n'); // -> rename: notes.txt
await fsp.appendFile('watched/notes.txt', 'again\n'); // -> change: notes.txt

// Without this the watcher keeps the event loop alive forever.
setTimeout(() => controller.abort(), 500);

When to use it

  • Restart or reload a dev server when a source file under src/ is saved.
  • Reload application configuration without a full restart when config.json changes.
  • Trigger an asset rebuild the moment a stylesheet or template is edited.
  • Pick up files dropped into an inbox directory by another process.

Frequently asked questions

Why does fs.watch fire multiple times for a single file save?

Because most editors do not write in place — they write a temporary file, rename it over the target, then update its metadata, and each of those steps is a separate OS event. Debounce the listener with a short timer, typically 50-200 ms keyed by filename, so one save produces one action.

How do I stop fs.watch and let the process exit?

Call watcher.close(), or pass an AbortSignal in options and abort its controller. A watcher is persistent by default and keeps the event loop alive indefinitely; setting persistent: false lets the process exit while the watcher is still active.

What is the difference between fs.watch and fs.watchFile?

fs.watch() uses native OS notifications and is fast and low-cost, while fs.watchFile() polls with stat() on an interval and reports the previous and current Stats. Prefer fs.watch() in general, and fall back to fs.watchFile() on network filesystems such as NFS or SMB, where native notifications are often unreliable or absent.

More examples

Async iterator form with a timeout

Each loop iteration logs one event, for example 'change src/app.js', and after five seconds the abort ends the loop with an AbortError that the catch turns into 'stopped watching'. The async-iterator form suits sequential handling — the loop body runs to completion before the next event is taken from the queue — where the callback form would run handlers concurrently. Note the events are objects with eventType and filename properties, not two separate arguments.

Example · javascript
import { watch } from 'node:fs/promises';

const ac = new AbortController();
setTimeout(() => ac.abort(), 5000); // stop watching after 5 seconds

try {
  for await (const event of watch('src', { recursive: true, signal: ac.signal })) {
    console.log(event.eventType, event.filename);
  }
} catch (err) {
  if (err.name === 'AbortError') console.log('stopped watching');
  else throw err;
}

console.log('done');

Debouncing the duplicate events

Saving one file prints a single 'rebuild triggered by app.js' line rather than the two or three raw events the OS delivered. Editors typically write to a temp file, rename it over the target and then touch its metadata, so the naive version runs your build several times per keystroke-save. The 100 ms window is per filename, so simultaneous edits to different files still each get their own rebuild.

Example · javascript
const fs = require('node:fs');

const timers = new Map();

fs.watch('src', { recursive: true }, (eventType, filename) => {
  if (!filename) return;            // filename can be null on some platforms
  if (!filename.endsWith('.js')) return;

  // One save often fires 2-3 events: collapse them into one rebuild.
  clearTimeout(timers.get(filename));
  timers.set(filename, setTimeout(() => {
    timers.delete(filename);
    console.log('rebuild triggered by', filename);
  }, 100));
});

console.log('watching src/ for .js changes');

Discussion

  • Be the first to comment on this lesson.