Skip to content

fix: Ensure walk() skips directories deleted before they are listed - #166

Open
nzakas wants to merge 1 commit into
mainfrom
fix/walk-list-enoent
Open

fix: Ensure walk() skips directories deleted before they are listed#166
nzakas wants to merge 1 commit into
mainfrom
fix/walk-list-enoent

Conversation

@nzakas

@nzakas nzakas commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

walk() crashed with ENOENT when a directory was deleted between being discovered and being listed — the race hit by ESLint users running other tools concurrently (#154, eslint/eslint#18955).

Root cause

NodeHfsImpl#list() and DenoHfsImpl#list() are async generators, so await impl.list(dir) only returns an iterator; readdir doesn't run until the first next(). The try/catch from #135 wrapped only the list() call, so the ENOENT surfaced inside for await, outside the catch. The #135 test didn't catch this because its mock list() was a plain function that threw synchronously.

Fix

walk() now drives the list() iterator manually (Symbol.asyncIterator, falling back to Symbol.iterator so array-returning impls still work) and wraps each next():

  • ENOENT → stop walking that directory silently (entries already yielded from it are kept)
  • any other error → rethrown
  • on early exit (consumer break, filter throwing) the iterator's return() is called, matching for await cleanup semantics

The existing catch around the list() call is kept for impls that throw synchronously.

Tests

  • core (6 new, mock async-generator list()): skip on ENOENT at first next(); walked root missing → no entries; ENOENT partway through iteration; non-ENOENT errors rethrow; iterators closed on early break and on entryFilter throw. Also fixed the "ENONENT" typo in the existing test name.
  • node and deno (1 each, real filesystem): directoryFilter deletes the subdirectory after walk() discovers it but before it's listed — the exact race from the issue.

Verified the new tests reproduce the bug: against the unfixed hfs.js, the 3 core ENOENT tests and the Node real-FS test fail; with the fix, core 191 / node 199 / deno 125 steps pass.

Fixes #154

🤖 Generated with Claude Code

https://claude.ai/code/session_019DHXVwqQ4iawKrV2DxPVKV

`NodeHfsImpl#list()` and `DenoHfsImpl#list()` are async generators, so
the directory isn't actually read until the iterator is first advanced.
The ENOENT handling added in #135 only wrapped the call to `list()`,
so an ENOENT thrown during iteration (e.g. a directory deleted between
being discovered and being listed) escaped `walk()`.

`walk()` now drives the `list()` iterator manually, catching ENOENT from
`next()` and stopping the walk of that directory, while still rethrowing
other errors and closing the iterator on early exit.

Fixes #154

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DHXVwqQ4iawKrV2DxPVKV
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.

Bug: walk()/list() fails with ENOENT if directory is deleted

1 participant