{"record":{"id":"4f49ae00f4a4458c","repo":"denoland/deno","slug":"eisdir-4f49ae","errorCode":"EISDIR","errorMessage":"is a directory","messagePattern":"is a directory","errorType":"exception","errorClass":"ERR_FS_EISDIR","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/internal/fs/utils.mjs","lineNumber":1050,"sourceCode":"    options = validateRmdirOptions(options, defaultRmOptions);\n    validateBoolean(options.force, \"options.force\");\n\n    lstat(path, (err, stats) => {\n      if (err) {\n        if (options.force && err.code === \"ENOENT\") {\n          return cb(null, options);\n        }\n        return cb(err, options);\n      }\n\n      if (expectDir && !stats.isDirectory()) {\n        return cb(false);\n      }\n\n      if (stats.isDirectory() && !options.recursive) {\n        return cb(\n          new ERR_FS_EISDIR({\n            code: \"EISDIR\",\n            message: \"is a directory\",\n            path,\n            syscall: \"rm\",\n            errno: osConstants.errno.EISDIR,\n          }),\n        );\n      }\n      return cb(null, options);\n    });\n  },\n);\n\n/** @type {(path: string, options: RmOptions, expectDir: boolean) => RmOptions | false} */\nexport const validateRmOptionsSync = hideStackFrames(\n  (path, options, expectDir) => {\n    options = validateRmdirOptions(options, defaultRmOptions);\n    validateBoolean(options.force, \"options.force\");\n","sourceCodeStart":1032,"sourceCodeEnd":1068,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/internal/fs/utils.mjs#L1032-L1068","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nfs.rm('./build', (err) => { if (err) throw err; });\n// ERR_FS_EISDIR: is a directory, syscall 'rm'\n\n// after\nfs.rm('./build', { recursive: true, force: true }, (err) => {\n  if (err) throw err;\n});","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nfunction rmSafe(p) {\n  const st = fs.lstatSync(p, { throwIfNoEntry: false });\n  return st?.isDirectory()\n    ? fs.promises.rm(p, { recursive: true, force: true })\n    : fs.promises.rm(p, { force: true });\n}","typeGuard":null,"tryCatchPattern":"fs.rm(path, { force: true }, (err) => {\n  if (err && err.code === 'ERR_FS_EISDIR') {\n    return fs.rm(path, { recursive: true, force: true }, cb); // dir after all\n  }\n  // handle err or success\n});","preventionTips":["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"],"tags":["deno","node-compat","fs","rm","eisdir"],"backgroundTag":"is-a-directory","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}