denoland/deno · error · ERR_FS_EISDIR

EISDIR

EISDIR

Error message

${src} is a directory (not copied)

What it means

Thrown directly by the JS slow path of Deno's cp polyfill when src is a directory and options.recursive is falsy: '<src> is a directory (not copied)' with code EISDIR (cp.ts:187-194). This path runs when the options disable the native fast path — i.e. when filter, dereference: true, force: false, errorOnExist: true, preserveTimestamps: true, verbatimSymlinks: true, or a nonzero mode is set (canUseNativeFastPath, cp.ts:168-176). Same rule as the Rust-detected variant: directories require an explicit recursive opt-in.

Source

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

    opts.mode === 0;
}

function getStatsForCopy(
  statInfo,
  src,
  dest,
  opts,
) {
  const { EISDIR, EINVAL } = lazyConstants();
  if ((statInfo & CpEntryFlags.IsSrcDirectory) && opts.recursive) {
    return onDir(statInfo, src, dest, opts);
  } else if (statInfo & CpEntryFlags.IsSrcDirectory) {
    throw new ERR_FS_EISDIR({
      message: `${src} is a directory (not copied)`,
      path: src,
      syscall: "cp",
      errno: EISDIR,
      code: "EISDIR",
    });
  } else if (
    (statInfo & CpEntryFlags.IsSrcFile) ||
    (statInfo & CpEntryFlags.IsSrcCharDevice) ||
    (statInfo & CpEntryFlags.IsSrcBlockDevice)
  ) {
    return onFile(statInfo, src, dest, opts);
  } else if (statInfo & CpEntryFlags.IsSrcSymlink) {
    const isDestExists = !!(statInfo & CpEntryFlags.IsDestExists);
    return onLink(isDestExists, src, dest, opts);
  } else if (statInfo & CpEntryFlags.IsSrcSocket) {
    throw new ERR_FS_CP_SOCKET({
      message: `cannot copy a socket file: ${dest}`,
      path: dest,
      syscall: "cp",
      errno: EINVAL,
      code: "EINVAL",
    });

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Add recursive: true to the same options object.
  2. If only files should be copied, iterate entries and dispatch files individually (your filter then drives selection).
  3. lstat src up front and fail fast with your own error if a directory arrives without recursive.

Example fix

// before
await fs.promises.cp('./site', './out', { dereference: true });
// EISDIR: ./site is a directory (not copied)

// after
await fs.promises.cp('./site', './out', { recursive: true, dereference: true });
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
async function cpTyped(src, dest, opts = {}) {
  const isDir = (await fs.promises.lstat(src)).isDirectory();
  if (isDir && !opts.recursive) {
    throw new Error(`cp of directory ${src} requires { recursive: true }`);
  }
  return fs.promises.cp(src, dest, { ...opts, recursive: isDir ? true : opts.recursive });
}

Try / catch

try {
  await fs.promises.cp(dir, dest, { dereference: true });
} catch (err) {
  if (err.code === 'EISDIR' && err.syscall === 'cp') {
    await fs.promises.cp(dir, dest, { recursive: true, dereference: true });
  } else throw err;
}

Prevention

When it happens

Trigger: fs.cp(dir, dest, { dereference: true }) without recursive; fs.cp(dir, dest, { filter: fn }) or { preserveTimestamps: true } forgetting recursive; any option combination that forces the JS entry loop.

Common situations: Symlink-resolving deploys (dereference) or timestamp-preserving backups that forgot recursive; filter-based partial copies over directories; code that only tested the default-options fast path.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/f6d4e6a70b7b84d5. Report an issue: GitHub.