denoland/deno · error · Error

ERR_DIR_CLOSED

ERR_DIR_CLOSED

Error message

Directory handle was closed

What it means

ERR_DIR_CLOSED ('Directory handle was closed') is thrown by Dir.readSync() and the async read path after close()/closeSync() has been called on the handle. The class tracks a #closed flag; any read attempt afterwards fails before touching the filesystem. The error carries code ERR_DIR_CLOSED, matching Node.

Solutions

  1. Set the handle reference to null after closing and check it before every read
  2. Pair opendir with exactly one consumer - the scope that reads is the scope that closes
  3. For early exit use break and let the owning scope perform close()

Example fix

// before
const dir = fs.opendirSync(path);
for (;;) {
  const ent = dir.readSync();
  if (!ent) break;
  if (ent.name === 'stop') dir.closeSync(); // then loop calls readSync again
}

// after
const dir = fs.opendirSync(path);
let closed = false;
try {
  for (;;) {
    const ent = dir.readSync();
    if (!ent) break;
    if (ent.name === 'stop') break;
  }
} finally {
  if (!closed) { dir.closeSync(); closed = true; }
}
Defensive patterns

Strategy: validation

Validate before calling

class SafeDir {
  #dir: fs.Dir<string>;
  #closed = false;
  constructor(path: string) { this.#dir = fs.opendirSync(path); }
  readSync() {
    if (this.#closed) throw new Error('dir already closed');
    return this.#dir.readSync();
  }
  closeSync() {
    if (!this.#closed) { this.#dir.closeSync(); this.#closed = true; }
  }
}

Try / catch

try {
  const entry = dir.readSync();
} catch (e) {
  if ((e as NodeJS.ErrnoException).code === 'ERR_DIR_CLOSED') {
    break; // iteration finished by another path - stop cleanly
  }
  throw e;
}

Prevention

When it happens

Trigger: const d = fs.opendirSync(dir); d.closeSync(); d.readSync(); a cleanup/error path closing a shared handle while another loop still reads it; double iteration where the first consumer closes the handle.

Common situations: Helper functions that close handles on error while their caller continues iterating; early-break loops followed by a finally that reads once more; refactors that move close() earlier in the lifecycle.

Related errors


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

Appendix: 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 9ad36f7a2c)