#1476·chokidar

A symlink whose target path passes through a regular file (ENOTDIR) silently disables watching of its directory and prevents `ready`

Author: talkasabCreated Aug 6, 2026Updated Aug 6, 2026

Describe the bug

If a watched directory contains a symlink whose target path passes through a regular file (so realpath() fails with ENOTDIR), chokidar silently stops watching that directory and the watcher's ready event never fires. No error event is emitted, and no events are ever delivered for sibling files in that directory.

A merely dangling symlink (ENOENT) behaves fine, which makes this easy to misdiagnose. Links like this occur in practice when build output changes shape (e.g. a stale dist/lib.js/index.js link after dist/lib.js became a file).

Versions (please complete the following information):

  • Chokidar version: 5.0.0 (readdirp 5.1.1); also reproduced on 4.0.3
  • Node version: 26.7.0
  • OS version: Linux (arm64); also reproduced under Bun 1.3.14

To Reproduce:

javascript
import { mkdtempSync, writeFileSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import chokidar from "chokidar";

const root = mkdtempSync(join(tmpdir(), "enotdir-"));
writeFileSync(join(root, "regular.txt"), "hi\n");
// Target path passes THROUGH regular.txt → realpath() rejects with ENOTDIR.
// (Replace with a dangling target, e.g. join(root, "missing"), and
// everything works — that's the control case.)
symlinkSync(join(root, "regular.txt", "nope"), join(root, "poison"));

const w = chokidar.watch(root, { followSymlinks: false });
w.on("ready", () => console.log("ready"));
w.on("error", (e) => console.log("error", e.code));
w.on("all", (ev, p) => console.log(ev, p));
setTimeout(() => writeFileSync(join(root, "regular.txt"), "edit\n"), 500);

// Expected: "ready", then add/change for regular.txt (or at minimum an
//           "error" so the caller can react).
// Observed: only the initial addDir for the root. No ready, no error, no
//           add/change for regular.txt — ever.

Happy to contribute this as a test case for the test suite if that's useful.

Expected behavior

Either treat a failed realpath() on a symlink entry the same as the ENOENT case (skip the entry, keep watching the rest of the directory, fire ready), or surface an error event — anything but silent partial coverage with ready withheld.

Additional context

The mechanism appears to be in readdirp rather than chokidar itself: _getEntryType() calls realpath() on symlink entries and routes failures to _onError(), whose NORMAL_FLOW_ERRORS set (ENOENT, EPERM, EACCES, ELOOP, recursive) turns known errnos into warn — but anything else, including ENOTDIR, calls destroy(err) on the stream (readdirp 5.1.1 index.js). The destroyed listing takes the directory's watches and the pending ready accounting with it. A one-line fix may be adding ENOTDIR to the survivable set — a symlink whose target can't resolve isn't a directory to descend either way — but I'm happy to move this issue to the readdirp repo or send a PR to either project if you'd prefer.

Found while building peruse (a local directory viewer that watches served directories); we currently work around it by detecting the stalled scan and covering affected directories with fs.watch.