Node.jsintermediate
Use fs.cp for simple directory copies
Published July 9, 2027
import { cp } from "node:fs/promises";
export const copyDirectory = (src: string, dest: string, options: { overwrite?: boolean } = {}) =>
cp(src, dest, {
recursive: true,
force: options.overwrite ?? false,
errorOnExist: !options.overwrite,
filter: (source, destination) =>
!source.includes("node_modules") && !destination.includes(".cache"),
});What
copyDirectory(src, dest) recursively copies a directory with node:fs/promises' cp, skipping node_modules and .cache via a filter, with an explicit overwrite switch controlling what happens if dest already exists.
Why it matters
Before fs.cp, recursively copying a directory in Node meant either shelling out to cp -r (not cross-platform — there's no cp on a bare Windows install — and gives you no filtering or structured error handling), or hand-rolling a walk with fs.readdir + fs.mkdir + fs.copyFile recursion. fs.cp does that walk internally and exposes a filter hook and explicit conflict handling, without a subprocess or a dependency like fs-extra.
How it works
recursive: trueis required for directory sources —cpis built to copy single files too, so recursion isn't the default and it throws on a directory without it.filterreceives both the source and destination path for every entry being copied and returns a boolean (or aPromise<boolean>). Returningfalsefor a directory skips that directory and everything inside it, not just the one entry.forceanderrorOnExistinteract rather than acting as independent switches.forcedefaults totrue— an existing destination is silently overwritten unless you turn it off.errorOnExistonly has an effect whenforceisfalse; it's ignored entirely whenforceistrue. Passingoverwrite: truehere setsforce: true(soerrorOnExistdoesn't matter), and the default (overwrite: false) setsforce: false, errorOnExist: true— an existing destination throws instead of getting silently touched.
Gotchas
fs.cp's own default (if you don't touch the options at all) is to overwrite —cp(src, dest, { recursive: true })alone silently clobbers an existingdest. Being explicit aboutforce/errorOnExist, like this wrapper is, avoids relying on that default.- Copying is not atomic. A crash partway through a large tree leaves a partially-copied destination with no automatic rollback.
fs.cponly became stable (non-experimental) in Node 22.3.0, after shipping experimental back in 16.7.0 — worth checking your Node version if a specific option doesn't seem to behave as documented.
Related
fs.copyFile— the single-file primitive this builds on; reach for it directly when you know you're copying exactly one file and don't need recursion or filtering.child_process.exec("cp -r ...")— the pre-fs.cpescape hatch. Still works, but isn't cross-platform and gives you no filter hook or structured error codes.