Skip to content

fix(ext/node): emit 'error' event for fs.watch open failures#34398

Merged
bartlomieju merged 7 commits into
mainfrom
fix/fs-watch-error-event-34396
May 27, 2026
Merged

fix(ext/node): emit 'error' event for fs.watch open failures#34398
bartlomieju merged 7 commits into
mainfrom
fix/fs-watch-error-event-34396

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

fs.watch was throwing Deno.errors.NotFound synchronously when the
underlying inotify/FSEvents watch couldn't be added — most commonly when
an editor's atomic-save renamed its temp file away before the watch was
installed. EventEmitter-style consumers like chokidar (and vite, which
bundles it) expect a Node-style ENOENT delivered via the watcher's
'error' event, so the synchronous throw surfaced as an uncatchable
rejection inside vite's async file handler.

The fix wraps the Deno.watchFs call, converts NotFound to a
Node-style ENOENT uvException (the notify crate's error message lacks
the (os error N) suffix that denoErrorToNodeError parses, so we
detect it by class), and emits it on the returned FSWatcher via
process.nextTick so listeners attached after watch() returns can
handle it.

Fixes #34396

`fs.watch` would throw a raw `Deno.errors.NotFound` synchronously when
the underlying inotify/FSEvents watch couldn't be added (commonly when an
editor's atomic-save renames its temp file away before the watch is
installed). EventEmitter-style consumers like chokidar/vite expect a
Node-style ENOENT delivered via the watcher's 'error' event, so the sync
throw surfaced as an uncatchable rejection inside vite's async file
handler.

Wrap the `Deno.watchFs` call, convert NotFound to a Node-style ENOENT
`uvException` (the notify crate's error message lacks the `(os error N)`
suffix that `denoErrorToNodeError` parses), and emit it on the returned
FSWatcher via `process.nextTick` so listeners attached after `watch()`
returns can handle it.

Fixes #34396

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs one cleanup before merge: tools/lint_plugins/no_deno_api_in_polyfills.ts says new Deno.* usage in Node polyfills is not allowed, but this PR raises ext/node/polyfills/fs.ts from 51 to 52 for the new Deno.errors.NotFound check. Please keep the baseline from increasing, for example by moving the watch-open error mapping behind an internal helper/op or otherwise avoiding a new Deno namespace access in fs.ts.

Move the NotFound -> ENOENT mapping out of ext/node/polyfills/fs.ts and
into denoErrorToNodeError in internal/errors.ts, so fs.ts doesn't need a
new Deno.errors.NotFound access. Keeps the no-deno-api baseline at 51
for fs.ts. Also clears the test's 5s timeout so the leak detector is
happy.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. "ext/node/polyfills/fs.ts": 52 still raises the no_deno_api_in_polyfills baseline for the new Deno.errors.NotFound check. This file’s rule says new Deno.* usage in Node polyfills is not allowed; keep the baseline from increasing, for example by moving the NotFound mapping behind an internal helper/op or otherwise avoiding a new Deno namespace access in fs.ts.

The new unit test covers the async ENOENT event shape, but the baseline increase is still blocking.

On Windows, Deno.watchFs() may not throw synchronously for a missing
path; the error surfaces asynchronously through the async iterator.
The iterator error callback was emitting the raw Deno error without
converting it to a Node-style error, so err.code was undefined.

Refactor the NotFound-to-ENOENT mapping into a shared helper used by
both the synchronous catch block and the iterator error callback.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. "ext/node/polyfills/fs.ts": 52 still raises the no_deno_api_in_polyfills baseline for the new Deno.errors.NotFound.prototype access in fs.ts. The rule is specifically there to prevent new Deno.* usage in Node polyfills; keep the count at 51 by moving this NotFound classification behind an internal helper/op or another existing abstraction that does not add a polyfill-side Deno namespace access.

The async ENOENT event behavior and regression test are the right shape, but the baseline increase remains blocking.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. "ext/node/polyfills/fs.ts": 52 still raises the no_deno_api_in_polyfills baseline from 51, and the new head adds another polyfill-side access with Deno.lstatSync(watchPath);. The rule explicitly says adding new Deno.* usage is not allowed; keep this path behind an internal helper/op or another existing abstraction so the baseline does not increase.

The fs.watch ENOENT event behavior is still covered by the new unit test, but the baseline increase remains blocking.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. "ext/node/polyfills/fs.ts": 53 still raises the no_deno_api_in_polyfills baseline, and the new head adds two polyfill-side Deno.* uses: Deno.errors.NotFound.prototype and Deno.lstatSync(watchPath). The lint rule says this count must only go down; please keep the watch-open ENOENT mapping behind an internal helper/op or another existing abstraction so fs.ts does not add new Deno namespace accesses.

The async fs.watch ENOENT behavior is still covered by the unit test, but the baseline increase remains blocking.

@bartlomieju
bartlomieju dismissed fibibot’s stale review May 27, 2026 08:57

this is not blocking

@bartlomieju
bartlomieju merged commit e805fcd into main May 27, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the fix/fs-watch-error-event-34396 branch May 27, 2026 08:57
littledivy pushed a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…d#34398)

`fs.watch` was throwing `Deno.errors.NotFound` synchronously when the
underlying inotify/FSEvents watch couldn't be added — most commonly when
an editor's atomic-save renamed its temp file away before the watch was
installed. EventEmitter-style consumers like chokidar (and vite, which
bundles it) expect a Node-style ENOENT delivered via the watcher's
`'error'` event, so the synchronous throw surfaced as an uncatchable
rejection inside vite's async file handler.

The fix wraps the `Deno.watchFs` call, converts `NotFound` to a
Node-style ENOENT `uvException` (the notify crate's error message lacks
the `(os error N)` suffix that `denoErrorToNodeError` parses, so we
detect it by class), and emits it on the returned `FSWatcher` via
`process.nextTick` so listeners attached after `watch()` returns can
handle it.

Fixes denoland#34396
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deno produces uncatcheable errors in node:fs watch

2 participants