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

  1. Establish a single owner for the Dir and forbid reads after close
  2. Track closed state in your own wrapper and guard every read
  3. Open a fresh fs.opendir when re-iteration is genuinely needed
  4. 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

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


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/3f4405b2d492c8c6. Report an issue: GitHub.