{"record":{"id":"d8f84c505860ad1d","repo":"denoland/deno","slug":"err-fs-eisdir","errorCode":"ERR_FS_EISDIR","errorMessage":"Path is a directory","messagePattern":"Path is a directory","errorType":"exception","errorClass":"NodeSystemError","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/_fs/cp/cp.ts","lineNumber":92,"sourceCode":"      });\n    case \"NON_DIR_TO_DIR\":\n      throw new ERR_FS_CP_NON_DIR_TO_DIR({\n        message: err.message,\n        path: err.path,\n        syscall: \"cp\",\n        errno: ENOTDIR,\n        code: \"ENOTDIR\",\n      });\n    case \"EEXIST\":\n      throw new ERR_FS_CP_EEXIST({\n        message: err.message,\n        path: err.path,\n        syscall: \"cp\",\n        errno: EEXIST,\n        code: \"EEXIST\",\n      });\n    case \"EISDIR\":\n      throw new ERR_FS_EISDIR({\n        message: err.message,\n        path: err.path,\n        syscall: \"cp\",\n        errno: EISDIR,\n        code: \"EISDIR\",\n      });\n    case \"SOCKET\":\n      throw new ERR_FS_CP_SOCKET({\n        message: err.message,\n        path: err.path,\n        syscall: \"cp\",\n        errno: EINVAL,\n        code: \"EINVAL\",\n      });\n    case \"FIFO\":\n      throw new ERR_FS_CP_FIFO_PIPE({\n        message: err.message,\n        path: err.path,","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/denoland/deno/blob/89f33cbef296a2b287f323d42de54c871fa69c77/ext/node/polyfills/_fs/cp/cp.ts#L74-L110","documentation":"Inside fs.cp, when the native op reports kind 'EISDIR', throwCpError rethrows it as ERR_FS_EISDIR with default message 'Path is a directory': an operation that requires a plain file (open/write path taken by the file-copy branch) hit a directory during the copy — e.g. the destination turned out to be a directory where a file write was attempted in a combination not caught by the DIR/NON_DIR pre-checks.","triggerScenarios":"Race where dest is created as a directory between validation and write; copying with options that take the on-file branch while dest resolves to a directory (e.g. through symlinks/dereference combos the native op rejects with EISDIR).","commonSituations":"Concurrent build jobs writing the same output path; symlinked destination directories with dereference enabled; transient states during watch-mode rebuilds.","solutions":["Re-stat src and dest with fsp.lstat right before copying and bail or clean on mismatch","Avoid concurrent copies to the same destination (serialize with a lock or unique temp names)","Retry once after removing the conflicting dest if your tooling allows it"],"exampleFix":"// before\nawait fsp.cp(src, dest, opts); // concurrent job made dest a directory mid-run\n\n// after\nconst st = await fsp.lstat(dest).catch(() => null);\nif (st?.isDirectory() && !(await fsp.lstat(src)).isDirectory()) {\n  await fsp.rm(dest, { recursive: true, force: true });\n}\nawait fsp.cp(src, dest, opts);","handlingStrategy":"retry","validationCode":"const [s, d] = await Promise.all([fsp.lstat(src), fsp.lstat(dest).catch(() => null)]);\nif (d?.isDirectory() && !s.isDirectory()) await fsp.rm(dest, { recursive: true, force: true });","typeGuard":null,"tryCatchPattern":"try {\n  await fsp.cp(src, dest, opts);\n} catch (err) {\n  if (err.code === 'ERR_FS_EISDIR' && !err.message.includes('not copied')) {\n    await fsp.rm(dest, { recursive: true, force: true });\n    await fsp.cp(src, dest, opts); // one retry after clearing conflicting dest\n  } else throw err;\n}","preventionTips":["Serialize concurrent copies that target the same dest path","Copy to a temp name and rename atomically when races are possible","Validate src/dest types immediately before the copy, not long before"],"tags":["node-compat","fs","cp","eisdir","race-condition"],"backgroundTag":null,"analyzedSha":"89f33cbef296a2b287f323d42de54c871fa69c77","analyzedAt":"2026-08-16T07:54:21.310Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}