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
- Pass { recursive: true } (usually with force: true): fs.rm(path, { recursive: true, force: true }, cb).
- If directories are unexpected, lstat first and branch or throw your own descriptive error.
- 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
- Default cleanup helpers to { recursive: true, force: true } when targets may be directories
- Remember force only tolerates ENOENT — it does not authorize directory removal
- lstat the target when its type is environment-dependent
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
- ERR_FS_CP_DIR_TO_NON_DIR
- ERR_FS_EISDIR
- ERR_FS_EISDIR
- EISDIR
- A file exists at the destination: ${destStr}
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/4f49ae00f4a4458c.
Report an issue: GitHub.