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
- Add recursive: true to the same options object.
- If only files should be copied, iterate entries and dispatch files individually (your filter then drives selection).
- 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
- Remember that dereference/filter/preserveTimestamps/force:false switch to the JS path — recursive is still required for dirs
- Test option-rich copies against directories, not just files
- Centralize cp option assembly in one helper so recursive is never forgotten
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.