From 6e0a5872093b08d7a8372a30e0b81eb46bf01c97 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Tue, 17 Feb 2026 20:19:51 +0100 Subject: [PATCH 1/7] watch: fix --env-file-if-exists crashing on linux if the file is missing --- lib/internal/main/watch_mode.js | 15 +++++++++++---- test/sequential/test-watch-mode.mjs | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 06c2c8602da444..6cbe684fe0c252 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -24,6 +24,7 @@ const { green, blue, red, white, clear } = require('internal/util/colors'); const { convertToValidSignal } = require('internal/util'); const { spawn } = require('child_process'); +const { existsSync } = require('fs'); const { inspect } = require('util'); const { setTimeout, clearTimeout } = require('timers'); const { resolve } = require('path'); @@ -34,10 +35,8 @@ markBootstrapComplete(); const kKillSignal = convertToValidSignal(getOptionValue('--watch-kill-signal')); const kShouldFilterModules = getOptionValue('--watch-path').length === 0; -const kEnvFiles = [ - ...getOptionValue('--env-file'), - ...getOptionValue('--env-file-if-exists'), -]; +const kEnvFiles = getOptionValue('--env-file'); +const kOptionalEnvFiles = getOptionValue('--env-file-if-exists'); const kWatchedPaths = ArrayPrototypeMap(getOptionValue('--watch-path'), (path) => resolve(path)); const kPreserveOutput = getOptionValue('--watch-preserve-output'); const kCommand = ArrayPrototypeSlice(process.argv, 1); @@ -105,6 +104,14 @@ function start() { if (kEnvFiles.length > 0) { ArrayPrototypeForEach(kEnvFiles, (file) => watcher.filterFile(resolve(file))); } + if (kOptionalEnvFiles.length > 0) { + ArrayPrototypeForEach(kOptionalEnvFiles, (file) => { + const resolvedPath = resolve(file); + if (existsSync(resolvedPath)) { + watcher.filterFile(resolvedPath); + } + }); + } child.once('exit', (code) => { exited = true; const waitingForChanges = 'Waiting for file changes before restarting...'; diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index a5cac129ad1c21..39ad7ef72ada00 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -277,6 +277,27 @@ describe('watch mode', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_00 } }); + it('should not crash when --env-file-if-exists points to a missing file', async () => { + const envKey = `TEST_ENV_${Date.now()}`; + const jsFile = createTmpFile(`console.log('ENV: ' + process.env.${envKey});`); + const missingEnvFile = path.join(tmpdir.path, `missing-${Date.now()}.env`); + const { done, restart } = runInBackground({ + args: ['--watch-path', tmpdir.path, `--env-file-if-exists=${missingEnvFile}`, jsFile], + }); + + try { + const { stderr, stdout } = await restart(); + + assert.doesNotMatch(stderr, /ENOENT: no such file or directory, watch/); + assert.deepStrictEqual(stdout, [ + 'ENV: undefined', + `Completed running ${inspect(jsFile)}. Waiting for file changes before restarting...`, + ]); + } finally { + await done(); + } + }); + it('should watch changes to a failing file', async () => { const file = createTmpFile('throw new Error("fails");'); const { stderr, stdout } = await runWriteSucceed({ From 635ff26bd913be34517d0576b97c0298a5a6f229 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Wed, 18 Feb 2026 17:03:30 +0100 Subject: [PATCH 2/7] watch: try/catch instead of existsSync --- lib/internal/main/watch_mode.js | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 6cbe684fe0c252..319f153c9f3aa0 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -16,19 +16,18 @@ const { } = require('internal/process/pre_execution'); const { triggerUncaughtException, - exitCodes: { kNoFailure }, + exitCodes: {kNoFailure}, } = internalBinding('errors'); -const { getOptionValue } = require('internal/options'); -const { FilesWatcher } = require('internal/watch_mode/files_watcher'); -const { green, blue, red, white, clear } = require('internal/util/colors'); -const { convertToValidSignal } = require('internal/util'); - -const { spawn } = require('child_process'); -const { existsSync } = require('fs'); -const { inspect } = require('util'); -const { setTimeout, clearTimeout } = require('timers'); -const { resolve } = require('path'); -const { once } = require('events'); +const {getOptionValue} = require('internal/options'); +const {FilesWatcher} = require('internal/watch_mode/files_watcher'); +const {green, blue, red, white, clear} = require('internal/util/colors'); +const {convertToValidSignal} = require('internal/util'); + +const {spawn} = require('child_process'); +const {inspect} = require('util'); +const {setTimeout, clearTimeout} = require('timers'); +const {resolve} = require('path'); +const {once} = require('events'); prepareMainThreadExecution(false, false); markBootstrapComplete(); @@ -83,7 +82,7 @@ for (let i = 0; i < process.execArgv.length; i++) { ArrayPrototypePushApply(argsWithoutWatchOptions, kCommand); -const watcher = new FilesWatcher({ debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all' }); +const watcher = new FilesWatcher({debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all'}); ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p)); let graceTimer; @@ -106,9 +105,13 @@ function start() { } if (kOptionalEnvFiles.length > 0) { ArrayPrototypeForEach(kOptionalEnvFiles, (file) => { - const resolvedPath = resolve(file); - if (existsSync(resolvedPath)) { - watcher.filterFile(resolvedPath); + try { + watcher.filterFile(resolve(file)); + } catch (error) { + if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') { + throw error; + } + // Failed watching the file, ignore } }); } @@ -134,7 +137,7 @@ async function killAndWait(signal = kKillSignal, force = false) { } const onExit = once(child, 'exit'); child.kill(signal); - const { 0: exitCode } = await onExit; + const {0: exitCode} = await onExit; return exitCode; } @@ -167,6 +170,7 @@ async function stop(child) { } let restarting = false; + async function restart(child) { if (restarting) return; restarting = true; @@ -205,5 +209,6 @@ function signalHandler(signal) { process.exit(exitCode ?? kNoFailure); }; } + process.on('SIGTERM', signalHandler('SIGTERM')); process.on('SIGINT', signalHandler('SIGINT')); From abb5f5a5f5bfa061f369bd963fce609943945f39 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Wed, 18 Feb 2026 18:44:20 +0100 Subject: [PATCH 3/7] watch: js-lint-fix --- lib/internal/main/watch_mode.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 319f153c9f3aa0..5f9608dcb14971 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -16,18 +16,18 @@ const { } = require('internal/process/pre_execution'); const { triggerUncaughtException, - exitCodes: {kNoFailure}, + exitCodes: { kNoFailure }, } = internalBinding('errors'); -const {getOptionValue} = require('internal/options'); -const {FilesWatcher} = require('internal/watch_mode/files_watcher'); -const {green, blue, red, white, clear} = require('internal/util/colors'); -const {convertToValidSignal} = require('internal/util'); +const { getOptionValue } = require('internal/options'); +const { FilesWatcher } = require('internal/watch_mode/files_watcher'); +const { green, blue, red, white, clear } = require('internal/util/colors'); +const { convertToValidSignal } = require('internal/util'); -const {spawn} = require('child_process'); -const {inspect} = require('util'); -const {setTimeout, clearTimeout} = require('timers'); -const {resolve} = require('path'); -const {once} = require('events'); +const { spawn } = require('child_process'); +const { inspect } = require('util'); +const { setTimeout, clearTimeout } = require('timers'); +const { resolve } = require('path'); +const { once } = require('events'); prepareMainThreadExecution(false, false); markBootstrapComplete(); @@ -82,7 +82,7 @@ for (let i = 0; i < process.execArgv.length; i++) { ArrayPrototypePushApply(argsWithoutWatchOptions, kCommand); -const watcher = new FilesWatcher({debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all'}); +const watcher = new FilesWatcher({ debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all' }); ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p)); let graceTimer; @@ -137,7 +137,7 @@ async function killAndWait(signal = kKillSignal, force = false) { } const onExit = once(child, 'exit'); child.kill(signal); - const {0: exitCode} = await onExit; + const { 0: exitCode } = await onExit; return exitCode; } From 82654419f68a6673525d80ce47410a490a866d23 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Thu, 19 Feb 2026 20:34:56 +0100 Subject: [PATCH 4/7] feat: dont throw on ENOENT if allowMissing is set --- lib/fs.js | 5 ++++- lib/internal/fs/recursive_watch.js | 10 +++++++--- lib/internal/fs/watchers.js | 9 +++++++-- lib/internal/main/watch_mode.js | 11 +---------- lib/internal/watch_mode/files_watcher.js | 10 +++++----- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index 4a03fada49ea8a..dfdc459b74ad8f 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -2488,6 +2488,7 @@ function appendFileSync(path, data, options) { * recursive?: boolean; * encoding?: string; * signal?: AbortSignal; + * throwIfNoEntry?: boolean; * }} [options] * @param {( * eventType?: string, @@ -2506,6 +2507,7 @@ function watch(filename, options, listener) { if (options.persistent === undefined) options.persistent = true; if (options.recursive === undefined) options.recursive = false; + if (options.throwIfNoEntry === undefined) options.throwIfNoEntry = true; let watcher; const watchers = require('internal/fs/watchers'); @@ -2523,7 +2525,8 @@ function watch(filename, options, listener) { options.persistent, options.recursive, options.encoding, - options.ignore); + options.ignore, + options.throwIfNoEntry); } if (listener) { diff --git a/lib/internal/fs/recursive_watch.js b/lib/internal/fs/recursive_watch.js index 136f69f585015e..f8cc9a5b7acd1e 100644 --- a/lib/internal/fs/recursive_watch.js +++ b/lib/internal/fs/recursive_watch.js @@ -51,7 +51,7 @@ class FSWatcher extends EventEmitter { assert(typeof options === 'object'); - const { persistent, recursive, signal, encoding, ignore } = options; + const { persistent, recursive, signal, encoding, ignore, throwIfNoEntry } = options; // TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support. if (recursive != null) { @@ -66,6 +66,10 @@ class FSWatcher extends EventEmitter { validateAbortSignal(signal, 'options.signal'); } + if (throwIfNoEntry != null) { + validateBoolean(throwIfNoEntry, 'options.throwIfNoEntry'); + } + if (encoding != null) { // This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE if (typeof encoding !== 'string') { @@ -76,7 +80,7 @@ class FSWatcher extends EventEmitter { validateIgnoreOption(ignore, 'options.ignore'); this.#ignoreMatcher = createIgnoreMatcher(ignore); - this.#options = { persistent, recursive, signal, encoding }; + this.#options = { persistent, recursive, signal, encoding, throwIfNoEntry }; } close() { @@ -222,7 +226,7 @@ class FSWatcher extends EventEmitter { this.#watchFolder(filename); } } catch (error) { - if (error.code === 'ENOENT') { + if (this.#options.throwIfNoEntry !== false && error.code === 'ENOENT') { error.filename = filename; throw error; } diff --git a/lib/internal/fs/watchers.js b/lib/internal/fs/watchers.js index a493c957966907..c540307d79a52d 100644 --- a/lib/internal/fs/watchers.js +++ b/lib/internal/fs/watchers.js @@ -35,7 +35,7 @@ const { } = internalBinding('fs'); const { FSEvent } = internalBinding('fs_event_wrap'); -const { UV_ENOSPC } = internalBinding('uv'); +const { UV_ENOSPC, UV_ENOENT } = internalBinding('uv'); const { EventEmitter } = require('events'); const { @@ -293,7 +293,8 @@ FSWatcher.prototype[kFSWatchStart] = function(filename, persistent, recursive, encoding, - ignore) { + ignore, + throwIfNoEntry = true) { if (this._handle === null) { // closed return; } @@ -313,6 +314,10 @@ FSWatcher.prototype[kFSWatchStart] = function(filename, recursive, encoding); if (err) { + if (!throwIfNoEntry && err === UV_ENOENT) { + return; + } + const error = new UVException({ errno: err, syscall: 'watch', diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 5f9608dcb14971..553ef975b03def 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -104,16 +104,7 @@ function start() { ArrayPrototypeForEach(kEnvFiles, (file) => watcher.filterFile(resolve(file))); } if (kOptionalEnvFiles.length > 0) { - ArrayPrototypeForEach(kOptionalEnvFiles, (file) => { - try { - watcher.filterFile(resolve(file)); - } catch (error) { - if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') { - throw error; - } - // Failed watching the file, ignore - } - }); + ArrayPrototypeForEach(kOptionalEnvFiles, (file) => watcher.filterFile(resolve(file), undefined, true)); } child.once('exit', (code) => { exited = true; diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 9c0eb1ed817c29..d88d9e35af4f8b 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -110,11 +110,11 @@ class FilesWatcher extends EventEmitter { return [...this.#watchers.keys()]; } - watchPath(path, recursive = true) { + watchPath(path, recursive = true, allowMissing = false) { if (this.#isPathWatched(path)) { return; } - const watcher = watch(path, { recursive, signal: this.#signal }); + const watcher = watch(path, { recursive, signal: this.#signal, throwIfNoEntry: !allowMissing }); watcher.on('change', (eventType, fileName) => { // `fileName` can be `null` if it cannot be determined. See // https://github.com/nodejs/node/pull/49891#issuecomment-1744673430. @@ -126,14 +126,14 @@ class FilesWatcher extends EventEmitter { } } - filterFile(file, owner) { + filterFile(file, owner, allowMissing = false) { if (!file) return; if (supportsRecursiveWatching) { - this.watchPath(dirname(file)); + this.watchPath(dirname(file),true, allowMissing); } else { // Having multiple FSWatcher's seems to be slower // than a single recursive FSWatcher - this.watchPath(file, false); + this.watchPath(file, false, allowMissing); } this.#filteredFiles.add(file); if (owner) { From e485116b016dd3665ba2fcd180157e30fa4d8e30 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Thu, 19 Feb 2026 22:28:53 +0100 Subject: [PATCH 5/7] fix: add default val to throwIfNoEntry --- lib/internal/fs/recursive_watch.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/internal/fs/recursive_watch.js b/lib/internal/fs/recursive_watch.js index f8cc9a5b7acd1e..c4ea6ec29e31df 100644 --- a/lib/internal/fs/recursive_watch.js +++ b/lib/internal/fs/recursive_watch.js @@ -51,7 +51,7 @@ class FSWatcher extends EventEmitter { assert(typeof options === 'object'); - const { persistent, recursive, signal, encoding, ignore, throwIfNoEntry } = options; + let { persistent, recursive, signal, encoding, ignore, throwIfNoEntry } = options; // TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support. if (recursive != null) { @@ -68,6 +68,8 @@ class FSWatcher extends EventEmitter { if (throwIfNoEntry != null) { validateBoolean(throwIfNoEntry, 'options.throwIfNoEntry'); + } else { + throwIfNoEntry = true; } if (encoding != null) { @@ -226,7 +228,7 @@ class FSWatcher extends EventEmitter { this.#watchFolder(filename); } } catch (error) { - if (this.#options.throwIfNoEntry !== false && error.code === 'ENOENT') { + if (!this.#options.throwIfNoEntry && error.code === 'ENOENT') { error.filename = filename; throw error; } From 3f567d49e8e461b5c872172814903695ae19472d Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Thu, 19 Feb 2026 22:52:02 +0100 Subject: [PATCH 6/7] feat: make options an object --- lib/internal/fs/recursive_watch.js | 7 ++++--- lib/internal/main/watch_mode.js | 3 ++- lib/internal/watch_mode/files_watcher.js | 10 ++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/internal/fs/recursive_watch.js b/lib/internal/fs/recursive_watch.js index c4ea6ec29e31df..8fddd02f2dc0c6 100644 --- a/lib/internal/fs/recursive_watch.js +++ b/lib/internal/fs/recursive_watch.js @@ -51,7 +51,8 @@ class FSWatcher extends EventEmitter { assert(typeof options === 'object'); - let { persistent, recursive, signal, encoding, ignore, throwIfNoEntry } = options; + const { persistent, recursive, signal, encoding, ignore } = options; + let { throwIfNoEntry } = options; // TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support. if (recursive != null) { @@ -68,7 +69,7 @@ class FSWatcher extends EventEmitter { if (throwIfNoEntry != null) { validateBoolean(throwIfNoEntry, 'options.throwIfNoEntry'); - } else { + } else { throwIfNoEntry = true; } @@ -228,7 +229,7 @@ class FSWatcher extends EventEmitter { this.#watchFolder(filename); } } catch (error) { - if (!this.#options.throwIfNoEntry && error.code === 'ENOENT') { + if (this.#options.throwIfNoEntry !== false && error.code === 'ENOENT') { error.filename = filename; throw error; } diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 553ef975b03def..5e2e97f2fd60f2 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -104,7 +104,8 @@ function start() { ArrayPrototypeForEach(kEnvFiles, (file) => watcher.filterFile(resolve(file))); } if (kOptionalEnvFiles.length > 0) { - ArrayPrototypeForEach(kOptionalEnvFiles, (file) => watcher.filterFile(resolve(file), undefined, true)); + ArrayPrototypeForEach(kOptionalEnvFiles, + (file) => watcher.filterFile(resolve(file), undefined, { allowMissing: true })); } child.once('exit', (code) => { exited = true; diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index d88d9e35af4f8b..33b34eb98b45e7 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -110,10 +110,12 @@ class FilesWatcher extends EventEmitter { return [...this.#watchers.keys()]; } - watchPath(path, recursive = true, allowMissing = false) { + watchPath(path, recursive = true, options = kEmptyObject) { if (this.#isPathWatched(path)) { return; } + const { allowMissing = false } = options; + const watcher = watch(path, { recursive, signal: this.#signal, throwIfNoEntry: !allowMissing }); watcher.on('change', (eventType, fileName) => { // `fileName` can be `null` if it cannot be determined. See @@ -126,14 +128,14 @@ class FilesWatcher extends EventEmitter { } } - filterFile(file, owner, allowMissing = false) { + filterFile(file, owner, options = kEmptyObject) { if (!file) return; if (supportsRecursiveWatching) { - this.watchPath(dirname(file),true, allowMissing); + this.watchPath(dirname(file), true, options); } else { // Having multiple FSWatcher's seems to be slower // than a single recursive FSWatcher - this.watchPath(file, false, allowMissing); + this.watchPath(file, false, options); } this.#filteredFiles.add(file); if (owner) { From 232e0a340f9cbae5ca68e33eed8a61d5421fb422 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Thu, 19 Feb 2026 23:26:10 +0100 Subject: [PATCH 7/7] doc, test: add throwIfNoEntry docs and test --- doc/api/fs.md | 5 +++++ test/parallel/test-fs-watch-enoent.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/api/fs.md b/doc/api/fs.md index 66d29bc80fbf18..07b86c5ebe450b 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -4786,6 +4786,9 @@ The `atime` and `mtime` arguments follow these rules: