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
- Pass { recursive: true } when the target may be a directory: fs.rmSync(dir, { recursive: true, force: true })
- If you only expect an empty directory, use fs.rmdir/fs.rmdirSync instead
- Pre-check with fs.lstatSync(path)?.isDirectory() and branch to recursive removal
- 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
- Default to { recursive: true, force: true } in cleanup helpers
- Treat rm targets as untrusted paths: lstat before choosing the removal strategy
- Do not rely on force:true to suppress EISDIR — only recursive:true does
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.