denoland/deno · error · NodeError
ERR_DIR_CLOSED
ERR_DIR_CLOSED
Error message
Directory handle was closed
What it means
fs.Dir (returned by fs.opendir) throws ERR_DIR_CLOSED when read()/readSync() is called after close()/closeSync(). The polyfill tracks closure in a private #closed flag; note that unlike Node it does not expose a dir.closed getter, so callers must track the lifecycle themselves.
Source
Thrown at ext/node/polyfills/_fs/_fs_dir.ts:96
iteratorResult.done
? null
: direntFromDeno(iteratorResult.value, this.#dirPath),
);
}
},
(err) => {
if (callback) {
callback(err);
}
reject(err);
},
);
});
}
readSync(): Dirent | null {
if (this.#closed) {
throw new ERR_DIR_CLOSED();
}
if (!this.#syncIterator) {
this.#syncIterator = Deno.readDirSync(this.path)![SymbolIterator]();
}
const iteratorResult = ArrayIteratorPrototypeNext(this.#syncIterator);
if (iteratorResult.done) {
return null;
} else {
return direntFromDeno(iteratorResult.value, this.#dirPath);
}
}
/**
* Unlike Node, Deno does not require managing resource ids for reading
* directories, and therefore does not need to close directories when
* finished reading.
*/View on GitHub (pinned to 89f33cbef2)
Solutions
- Establish a single owner for the Dir and forbid reads after close
- Track closed state in your own wrapper and guard every read
- Open a fresh fs.opendir when re-iteration is genuinely needed
- Prefer for-await...of on the dir, which closes it automatically at completion or break
Example fix
// before
const dir = fs.opendirSync(p);
let entry;
while ((entry = dir.readSync())) {
if (entry.name === "stop") dir.closeSync(); // then read again below
}
const first = dir.readSync(); // ERR_DIR_CLOSED
// after
const dir = fs.opendirSync(p);
let found = null;
let entry;
while ((entry = dir.readSync())) {
if (entry.name === "stop") { found = entry; break; }
}
dir.closeSync(); Defensive patterns
Strategy: type-guard
Validate before calling
// Node exposes dir.closed; Deno's polyfill does not - track it yourself.
class SafeDir {
#dir; closed = false;
constructor(path: string) { this.#dir = fs.opendirSync(path); }
readSync() {
if (this.closed) return null; // guard instead of throwing ERR_DIR_CLOSED
return this.#dir.readSync();
}
closeSync() {
if (!this.closed) { this.closed = true; this.#dir.closeSync(); }
}
} Type guard
function isOpenDir(d: { closed?: boolean } | null | undefined): boolean {
// dir.closed exists on Node; on Deno's polyfill it is undefined, so also
// treat 'undefined' as open only if your wrapper tracks closure itself.
return d != null && d.closed !== true;
} Prevention
- Give the Dir a single owner; close it exactly once in a finally block
- Prefer for-await...of over the dir - it closes automatically on completion or break
- Do not read from cleanup/error paths unless you know the dir is still open
- Remember the Deno polyfill has no dir.closed getter - track state in a wrapper
When it happens
Trigger: const d = fs.opendirSync(p); d.closeSync(); d.readSync(); reading in cleanup/finally blocks after the main loop closed the dir; helper functions where one consumer closes and another reads.
Common situations: Early-exiting iteration (break) followed by cleanup code that closes, then later code reading again; wrappers whose lifetime differs from their callers; error paths that close the dir and then attempt a final read.
Related errors
- ERR_MISSING_ARGS
- A file exists at the destination: ${destStr}
- No callback function supplied
- ERR_INVALID_ARG_TYPE
- No callback function supplied
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/3f4405b2d492c8c6.
Report an issue: GitHub.