denoland/deno · error · ERR_FS_EISDIR

EISDIR

EISDIR

Error message

is a directory

What it means

validateRmOptions (utils.mjs:1030-1061) runs before every async/callback/promise fs.rm call: it lstats the target and, when the target is a directory and options.recursive is falsy, invokes the callback with ERR_FS_EISDIR ('is a directory', syscall 'rm'). Node's fs.rm deliberately refuses to unlink directories without the recursive opt-in — unlike the deprecated fs.rmdir it never falls back to directory removal.

Source

Thrown at ext/node/polyfills/internal/fs/utils.mjs:1050

    options = validateRmdirOptions(options, defaultRmOptions);
    validateBoolean(options.force, "options.force");

    lstat(path, (err, stats) => {
      if (err) {
        if (options.force && err.code === "ENOENT") {
          return cb(null, options);
        }
        return cb(err, options);
      }

      if (expectDir && !stats.isDirectory()) {
        return cb(false);
      }

      if (stats.isDirectory() && !options.recursive) {
        return cb(
          new ERR_FS_EISDIR({
            code: "EISDIR",
            message: "is a directory",
            path,
            syscall: "rm",
            errno: osConstants.errno.EISDIR,
          }),
        );
      }
      return cb(null, options);
    });
  },
);

/** @type {(path: string, options: RmOptions, expectDir: boolean) => RmOptions | false} */
export const validateRmOptionsSync = hideStackFrames(
  (path, options, expectDir) => {
    options = validateRmdirOptions(options, defaultRmOptions);
    validateBoolean(options.force, "options.force");

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pass { recursive: true } (usually with force: true): fs.rm(path, { recursive: true, force: true }, cb).
  2. If directories are unexpected, lstat first and branch or throw your own descriptive error.
  3. For shared helpers, choose options from the target type instead of a fixed flag.

Example fix

// before
fs.rm('./build', (err) => { if (err) throw err; });
// ERR_FS_EISDIR: is a directory, syscall 'rm'

// after
fs.rm('./build', { recursive: true, force: true }, (err) => {
  if (err) throw err;
});
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
function rmSafe(p) {
  const st = fs.lstatSync(p, { throwIfNoEntry: false });
  return st?.isDirectory()
    ? fs.promises.rm(p, { recursive: true, force: true })
    : fs.promises.rm(p, { force: true });
}

Try / catch

fs.rm(path, { force: true }, (err) => {
  if (err && err.code === 'ERR_FS_EISDIR') {
    return fs.rm(path, { recursive: true, force: true }, cb); // dir after all
  }
  // handle err or success
});

Prevention

When it happens

Trigger: fs.rm('./build', cb) with no options; fs.promises.rm(cacheDir) where the cache path is a directory; rm(path, { force: true }) still throws because force only suppresses ENOENT, not EISDIR.

Common situations: Migrations from fs.rmdirSync(path, { recursive: true }) to fs.rm that dropped the options object; paths that are files on the dev machine but directories in CI; cleanup code that assumed rm works like the shell rm -r.

Related errors


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