denoland/deno · error · SystemError

ERR_FS_EISDIR

ERR_FS_EISDIR

Error message

Path is a directory: rm returned EISDIR (is a directory) ${path}

What it means

Thrown by Deno's node:fs compatibility layer when fs.rm/fs.rmSync (and fs.promises.rm) targets an existing directory without { recursive: true }. The polyfill runs lstatSync whenever force is false, rm is called from the rmdir-style path (expectDir), or recursive is unset; if the target is a directory and recursive is falsy it raises ERR_FS_EISDIR with errno EISDIR and syscall 'rm'. Note that { force: true } alone does NOT suppress this: the check still runs because !options.recursive is true. It mirrors Node.js's refusal to delete directories non-recursively.

Source

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

  },
);

/** @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");

    if (!options.force || expectDir || !options.recursive) {
      const isDirectory = lstatSync(path, { throwIfNoEntry: !options.force })
        ?.isDirectory();

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

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

    return options;
  },
);

// Lazy: reading `lazyProcess().default` at eval time TDZs while node:process
// is itself bootstrapping (cold node-defer path). Resolve on first warning.
let recursiveRmdirWarned;
export function emitRecursiveRmdirWarning() {
  recursiveRmdirWarned ??= lazyProcess().default.noDeprecation;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pass { recursive: true } when the target may be a directory: fs.rmSync(dir, { recursive: true, force: true })
  2. If you only expect an empty directory, use fs.rmdir/fs.rmdirSync instead
  3. Pre-check with fs.lstatSync(path)?.isDirectory() and branch to recursive removal
  4. If the path may not exist, also pass force: true to avoid a companion ENOENT throw

Example fix

// before
fs.rmSync(targetPath);

// after
fs.rmSync(targetPath, { recursive: true, force: true });
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs';

function removePath(p: string) {
  const st = fs.lstatSync(p, { throwIfNoEntry: false });
  if (st?.isDirectory()) fs.rmSync(p, { recursive: true, force: true });
  else fs.rmSync(p, { force: true });
}

Type guard

const isDirectory = (p: string) =>
  fs.lstatSync(p, { throwIfNoEntry: false })?.isDirectory() ?? false;

Try / catch

try {
  fs.rmSync(p);
} catch (err) {
  if (err?.code === 'ERR_FS_EISDIR' || err?.code === 'EISDIR') {
    fs.rmSync(p, { recursive: true, force: true });
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: fs.rmSync('/tmp/dir') or await fs.promises.rm(dirPath) on an existing directory; fs.rm(dir, callback) with no options; fs.rmSync(dir, { force: true }) without recursive (EISDIR still thrown); passing a file path variable that actually points to a directory (e.g. a temp path created with mkdir).

Common situations: Cleanup scripts deleting temp/build directories (dist/, node_modules/.cache) written for the old fs.rmdir recursive behavior; config or CLI code where the path comes from user input that may be a directory; CI steps that remove workspaces; passing options in the wrong argument position so recursive is never received.

Related errors


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