fs: auto-close Dir async iterator on exit#28895
Conversation
Node's fs.Dir async iterator closes the directory handle when it finishes — naturally, via break, or via a throw. Bun's iterator never closed the handle, so a subsequent dir.close() succeeded instead of throwing ERR_DIR_CLOSED. Wrap the yield in try/finally and call closeSync() from finally, swallowing ERR_DIR_CLOSED in case the user closed the handle manually. Fixes #28894
|
Updated 10:20 AM PT - Apr 5th, 2026
❌ Your commit
🧪 To try this PR locally: bunx bun-pr 28895That installs a local version of the PR into your bun-28895 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughReworks Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/js/node/fs.ts`:
- Around line 1139-1141: In the iterator cleanup where this.closeSync() is
called, change the empty catch to capture the thrown error and only swallow it
when the error code equals 'ERR_DIR_CLOSED'; for any other error rethrow it.
Locate the call to this.closeSync() (the iterator cleanup block) and replace the
bare catch with one that inspects the caught err (e.g., err && err.code ===
'ERR_DIR_CLOSED') to ignore that specific case and rethrow otherwise.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: ea04fea7-1119-4ca4-b97f-b15dd0d9ab6f
📒 Files selected for processing (2)
src/js/node/fs.tstest/regression/issue/28894.test.ts
…rrors Addresses two issues in the previous iterator fix: 1. Second iteration on a closed Dir silently re-yielded the cached entries because `yield*` does not mutate the backing array. Add a handle check at the top of the iterator — matches Node. 2. Swallowing all close() errors in the finally hid unexpected failures. Match Node exactly (lib/internal/fs/dir.js) and let close errors, including ERR_DIR_CLOSED from a manual close inside the loop, propagate out of the iterator.
There was a problem hiding this comment.
Previous concerns addressed: guard added, dead imports removed, errors propagate correctly. LGTM.
Extended reasoning...
Overview
This PR fixes fs.Dir's async iterator to auto-close the directory handle on exit (natural, break, or throw), matching Node.js behavior. Changes are confined to the Dir class in src/js/node/fs.ts and a new regression test file.
Security risks
None. This is a Node.js compatibility fix for directory handle lifecycle — no auth, crypto, or permission-sensitive code paths involved.
Level of scrutiny
Low. The change is small, well-scoped, and follows an established pattern already used by read()/readSync() in the same class. The fix mirrors Node's own lib/internal/fs/dir.js implementation.
Prior review concerns (resolved)
My previous inline comments flagged two issues that have since been addressed in follow-up commits:
- Missing closed-handle guard at the start of the async iterator — now present:
if (this.#handle < 0) throw $ERR_DIR_CLOSED(); - Dead imports in the test file — removed; the file now only imports from
node:fs/promises
Additionally, coderabbitai's concern about silently swallowing all close errors is moot: the current code calls this.closeSync() directly in finally without any try/catch, so all errors propagate. A dedicated test verifies this behavior.
Other factors
Five test cases cover all relevant scenarios (break, natural completion, thrown error, second iteration, manual close inside loop). The implementation is consistent with the existing guard pattern in the class.
|
Heads up: this PR fixes auto-close on exit (#28894), but it does not cover the closely related #31879, where a `close()` between `next()` pulls should make the next pull throw `ERR_DIR_CLOSED`. The reason is that the iterator still does `yield* entries` over a snapshot array and only checks `this.#handle` once, before the first pull. After the first `next()`, later pulls come straight from the materialized array without re-checking the handle. Verified against this exact build: The minimal change that covers both issues is to drive the loop through `this.read()` (which already throws `ERR_DIR_CLOSED` on a closed handle) instead of `yield*`: async *[Symbol.asyncIterator]() {
try {
let entry;
while ((entry = await this.read()) !== null) {
yield entry;
}
} finally {
if (this.#handle >= 0) this.close();
}
}#31830 already takes this approach (per-pull `read()` plus auto-close in `finally`) and ports the Node `opendir`/`Dir` tests that assert the close-between-pulls behavior, so that PR covers both #28894 and #31879. |
What
Node's
fs.Dirasync iterator closes the directory handle when it finishes — naturally, viabreak, or via a throw. Bun's iterator never calledclose(), so the handle leaked until theDirwas GC'd and a subsequentdir.close()succeeded instead of throwingERR_DIR_CLOSED.Repro (fixed)
Fix
Wrap the
yield*in try/finally and callcloseSync()in the finally block, swallowingERR_DIR_CLOSEDin case the user closed the handle manually — same shape as Node'slib/internal/fs/dir.jsiterator.Verification
test/regression/issue/28894.test.tscovers:breakAll four fail on
USE_SYSTEM_BUN=1(well, 3 — the fourth only asserts no throw) and pass onbun bd.Fixes #28894