Files created in a newly-added directory between its scan and fs.watch registration are silently missed (still present in v5; same root cause as #1112)
Describe the bug
When a new directory appears inside a watched tree, _handleDir (handler.js) processes it in this order:
await this._handleRead(...)— the async readdirp scan of the directory- only after the scan completes:
_watchWithNodeFs(dir, ...)— thefs.watchregistration
Any file created after the scan reads the directory entries but before the watch registers is lost: the scan didn't see it and no watcher existed to observe the event. The file gets no add until some unrelated later event in that directory triggers a re-scan.
This is the same race reported in #1112 (v3.5.1). That issue was closed as completed, but the ordering is unchanged in v5.0.0 (and current master, src/handler.ts) and the bug still reproduces.
A real-world sequence that hits the window is an atomic write into a fresh folder — mkdir -p, stage a temp file, rename over the target. The scan can catch only the temp file (whose pending add is then dropped by awaitWriteFinish when it disappears), and the rename to the final name fires in the unwatched gap: the target file is never reported.
A userland workaround is also harder than it looks: watcher.add(newDir) cannot recover the missed file, because add() re-runs the scan with initialAdd = true and _handleFile suppresses the add event under ignoreInitial: true (if (!(initialAdd && this.fsw.options.ignoreInitial))). Recovering requires a manual readdir reconciled against getWatched().
Versions (please complete the following information):
- Chokidar version: 5.0.0
- Node version: 24.x
- OS version: macOS 15 and Linux (Debian, kernel 6.x) — both reproduce
To Reproduce:
The window is a few milliseconds wide, so per #1112's technique, widen it deterministically by injecting a delay in _handleDir between the scan and the watch registration (this doesn't cause the race, it only makes it easy to hit):
--- a/handler.js
+++ b/handler.js
@@ _handleDir
await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
if (this.fsw.closed)
return;
}
+ if (!initialAdd) await new Promise((res) => setTimeout(res, 3000));
closer = this._watchWithNodeFs(dir, (dirPath, stats) => {Then:
import { watch } from "chokidar"
import { mkdtemp, mkdir, writeFile } from "node:fs/promises"
import { join } from "node:path"
import { tmpdir } from "node:os"
const root = await mkdtemp(join(tmpdir(), "chokidar-race-"))
const watcher = watch(root, { ignoreInitial: true })
watcher.on("all", (event, path) => console.log(`EVENT ${event} ${path}`))
await new Promise((ready) => watcher.on("ready", ready))
const dir = join(root, "new-dir")
await mkdir(dir)
// Land the file inside the widened window (scan done, watch not yet registered)
await new Promise((r) => setTimeout(r, 1000))
await writeFile(join(dir, "missed.txt"), "hi")
await new Promise((r) => setTimeout(r, 5000))
console.log("--- 5s later: was missed.txt ever added? ---")
// An unrelated later write triggers a re-scan, which finally surfaces it
await writeFile(join(dir, "second.txt"), "hi")
await new Promise((r) => setTimeout(r, 2000))
await watcher.close()Expected behavior
An add event for new-dir/missed.txt shortly after it is created.
Actual output
EVENT addDir /tmp/chokidar-race-XXXX/new-dir
--- 5s later: was missed.txt ever added? ---
EVENT add /tmp/chokidar-race-XXXX/new-dir/missed.txt <- only after second.txt forces a re-scan
EVENT add /tmp/chokidar-race-XXXX/new-dir/second.txtPossible fix
Register the fs.watch before scanning (the existing tracked-children bookkeeping already dedupes an entry seen by both the scan and an event), or re-run _handleRead once immediately after the watch registration to reconcile anything that landed in the gap.
Source: paulmillr/chokidar