denoland/deno · error · NodeSystemError

ERR_FS_EISDIR

ERR_FS_EISDIR

Error message

Path is a directory

What it means

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.

Source

Thrown at ext/node/polyfills/_fs/cp/cp.ts:92

      });
    case "NON_DIR_TO_DIR":
      throw new ERR_FS_CP_NON_DIR_TO_DIR({
        message: err.message,
        path: err.path,
        syscall: "cp",
        errno: ENOTDIR,
        code: "ENOTDIR",
      });
    case "EEXIST":
      throw new ERR_FS_CP_EEXIST({
        message: err.message,
        path: err.path,
        syscall: "cp",
        errno: EEXIST,
        code: "EEXIST",
      });
    case "EISDIR":
      throw new ERR_FS_EISDIR({
        message: err.message,
        path: err.path,
        syscall: "cp",
        errno: EISDIR,
        code: "EISDIR",
      });
    case "SOCKET":
      throw new ERR_FS_CP_SOCKET({
        message: err.message,
        path: err.path,
        syscall: "cp",
        errno: EINVAL,
        code: "EINVAL",
      });
    case "FIFO":
      throw new ERR_FS_CP_FIFO_PIPE({
        message: err.message,
        path: err.path,

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Re-stat src and dest with fsp.lstat right before copying and bail or clean on mismatch
  2. Avoid concurrent copies to the same destination (serialize with a lock or unique temp names)
  3. Retry once after removing the conflicting dest if your tooling allows it

Example fix

// before
await fsp.cp(src, dest, opts); // concurrent job made dest a directory mid-run

// after
const st = await fsp.lstat(dest).catch(() => null);
if (st?.isDirectory() && !(await fsp.lstat(src)).isDirectory()) {
  await fsp.rm(dest, { recursive: true, force: true });
}
await fsp.cp(src, dest, opts);
Defensive patterns

Strategy: retry

Validate before calling

const [s, d] = await Promise.all([fsp.lstat(src), fsp.lstat(dest).catch(() => null)]);
if (d?.isDirectory() && !s.isDirectory()) await fsp.rm(dest, { recursive: true, force: true });

Try / catch

try {
  await fsp.cp(src, dest, opts);
} catch (err) {
  if (err.code === 'ERR_FS_EISDIR' && !err.message.includes('not copied')) {
    await fsp.rm(dest, { recursive: true, force: true });
    await fsp.cp(src, dest, opts); // one retry after clearing conflicting dest
  } else throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: Concurrent build jobs writing the same output path; symlinked destination directories with dereference enabled; transient states during watch-mode rebuilds.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/d8f84c505860ad1d. Report an issue: GitHub.