fsPromises.copyFile()
Copies a file to a new path, overwriting the destination unless COPYFILE_EXCL is set.
fsPromises.copyFile(src, dest[, mode])- Added in Node.js v10.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$src | string | Buffer | URL | Source filename to copy. |
$dest | string | Buffer | URL | Destination filename of the copy operation. |
$modeoptional | integer= 0 | Optional modifiers controlling the copy, given as a bitwise OR of fs.constants.COPYFILE_EXCL (fail if dest exists), COPYFILE_FICLONE (attempt a copy-on-write reflink, fall back to a real copy) and COPYFILE_FICLONE_FORCE (attempt a reflink, fail if unsupported). |
Return value
Promise<undefined>Fulfills with undefined upon success.
Errors
Rejects with ENOENT if src does not exist or dest's parent directory is missing, EEXIST when mode includes COPYFILE_EXCL and dest exists, and EISDIR if either path is a directory.
Changelog
- PHP v14.0.0Related: fsPromises.cp() was added for copying directory trees; copyFile handles single files only.
- PHP v10.0.0Added in v10.0.0.
fsPromises.copyFile() copies one file's contents to another path. By default the destination is overwritten if it already exists — the copy is not conditional and does not ask.
When to reach for it
Use it to snapshot a file before mutating it, to stage an asset into a build directory, or to seed a config from a template. It moves the bytes inside the kernel where the platform allows, so it beats readFile-then-writeFile for anything sizeable and never holds the file in your heap.
The mode flags
COPYFILE_EXCL turns the call into create-or-fail, which is the safe choice when you must not clobber. COPYFILE_FICLONE asks for a copy-on-write reflink on filesystems that support it (APFS, Btrfs, XFS) and silently falls back to a byte copy elsewhere; COPYFILE_FICLONE_FORCE rejects instead of falling back.
What trips people up
destmust be a filename, not a target directory. Passing a directory rejects withEISDIRrather than copying into it.- It copies files only — use
fsPromises.cp()withrecursive: truefor a tree. - The destination's parent directory is not created for you.
Example
When to use it
- Snapshot a config file before writing a modified version.
- Copy a template file into a newly scaffolded project directory.
- Stage a built asset into a dist folder.
- Claim a destination filename atomically using COPYFILE_EXCL.
Frequently asked questions
Does fsPromises.copyFile overwrite an existing file?
Yes, by default. Pass fs.constants.COPYFILE_EXCL as the mode to make the call reject with EEXIST instead of clobbering the destination.
How do I copy a whole directory in Node.js?
Use fsPromises.cp(src, dest, { recursive: true }). copyFile handles exactly one file and rejects with EISDIR on a directory.
Why does copyFile fail with EISDIR?
Because dest was a directory. copyFile needs the full destination filename — join the source's basename onto the target directory yourself.
More examples
Timestamped backup before an in-place edit
Prints now: mode=staging and was: mode=production. Copying first means the original survives even if the transform throws halfway through.
import fs from 'node:fs/promises';
await fs.writeFile('app.conf', 'mode=production\n');
async function editWithBackup(file, transform) {
await fs.copyFile(file, `${file}.bak`);
const before = await fs.readFile(file, 'utf8');
await fs.writeFile(file, transform(before));
}
await editWithBackup('app.conf', (s) => s.replace('production', 'staging'));
console.log('now: ', (await fs.readFile('app.conf', 'utf8')).trim());
console.log('was: ', (await fs.readFile('app.conf.bak', 'utf8')).trim());
await fs.rm('app.conf');
await fs.rm('app.conf.bak');Copying into a directory needs the filename
Prints directory as dest: EISDIR, then dist contains: [ 'logo.svg' ]. Unlike the cp shell command, copyFile never infers the destination name — path.join with basename is the fix.
import fs from 'node:fs/promises';
import path from 'node:path';
await fs.mkdir('dist', { recursive: true });
await fs.writeFile('logo.svg', '<svg/>');
try {
await fs.copyFile('logo.svg', 'dist');
} catch (err) {
console.log('directory as dest:', err.code);
}
await fs.copyFile('logo.svg', path.join('dist', path.basename('logo.svg')));
console.log('dist contains:', await fs.readdir('dist'));
await fs.rm('dist', { recursive: true });
await fs.rm('logo.svg');Requesting a copy-on-write clone
Prints sizes match: true 65536, then either the supported or the unsupported line depending on the filesystem. The point is the difference between the two flags: FICLONE degrades quietly, FICLONE_FORCE rejects, so only use FORCE when a reflink is a hard requirement.
import fs from 'node:fs/promises';
import { constants } from 'node:fs';
await fs.writeFile('data.bin', Buffer.alloc(64 * 1024, 7));
// FICLONE reflinks where supported and falls back silently elsewhere.
await fs.copyFile('data.bin', 'clone.bin', constants.COPYFILE_FICLONE);
const [a, b] = await Promise.all([fs.stat('data.bin'), fs.stat('clone.bin')]);
console.log('sizes match:', a.size === b.size, a.size);
try {
await fs.copyFile('data.bin', 'forced.bin', constants.COPYFILE_FICLONE_FORCE);
console.log('reflink supported here');
await fs.rm('forced.bin');
} catch (err) {
console.log('reflink unsupported:', err.code);
}
await fs.rm('data.bin');
await fs.rm('clone.bin');
Discussion