fs.rename()
Moves or renames a file or directory, replacing the destination if one already exists.
fs.rename(oldPath, newPath, callback)- Added in Node.js v0.0.2
Parameters
| Parameter | Type | Description |
|---|---|---|
$oldPath | string | Buffer | URL | The existing path to move. A URL must use the file: protocol. |
$newPath | string | Buffer | URL | The destination path. Its parent directory must already exist; an existing file at this path is replaced. |
$callback | Function | Completion callback, invoked as callback(err). Receives null on success. |
Return value
undefinedNothing. The callback is invoked with a possible error and no other arguments. The promise form, fsPromises.rename(oldPath, newPath), fulfills with undefined.
Errors
Passing a non-function callback throws ERR_INVALID_ARG_TYPE synchronously, while the callback receives ENOENT when oldPath is missing or newPath's parent directory does not exist, EXDEV when the two paths live on different filesystems, EISDIR or ENOTDIR on a file/directory type mismatch, and ENOTEMPTY when newPath is a non-empty directory.
Changelog
- PHP v18.0.0Passing an invalid callback to the callback argument now throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.
- PHP v10.0.0The callback parameter is no longer optional. Not passing it throws a TypeError at runtime. fsPromises.rename() was added in this release.
- PHP v7.6.0The oldPath and newPath parameters can be WHATWG URL objects using the file: protocol.
- PHP v7.0.0Omitting the callback started emitting a deprecation warning with id DEP0013.
fs.rename() asks the operating system to move a directory entry from one path to another. It is a single syscall, so on the same filesystem it costs the same whether the file is 1 KB or 100 GB — no bytes are copied. More importantly it is atomic: any other process reading the destination sees either the old file or the new one, never a half-written intermediate.
That atomicity is the main reason to reach for it. The standard safe-write pattern is to write a temporary file in the same directory, then rename it over the real path, so readers never observe a truncated file.
What trips people up
- EXDEV. A rename cannot cross filesystem boundaries. Moving between two Docker volumes, or from
/tmponto a mounted disk, fails withEXDEV; you needfs.copyFile()followed byfs.unlink()instead. Keeping the temp file in the destination directory sidesteps this entirely. - It overwrites silently. If
newPathalready exists as a file, it is replaced with no warning. There is no "fail if exists" flag. - Directories behave differently. Renaming onto an existing directory raises an error, and renaming a directory onto a non-empty one gives
ENOTEMPTY. - The parent of
newPathmust exist already — otherwiseENOENT.
The promise form takes the same two paths and fulfills with undefined.
Example
When to use it
- Publish a config or data file atomically by writing config.json.tmp and renaming it over config.json.
- Rotate a log file to app.log.1 before opening a fresh one.
- Move a finished upload out of a staging directory into its permanent name.
- Rename a whole directory tree in one call without touching its contents.
Frequently asked questions
How do I move a file in Node.js?
Use fs.rename(oldPath, newPath) — it is the direct equivalent of mv within a single filesystem. If the source and destination are on different mounts or volumes it fails with EXDEV, and you must fall back to fs.copyFile() followed by fs.unlink().
Why do I get EXDEV: cross-device link not permitted?
EXDEV means oldPath and newPath are on different filesystems, and the rename syscall cannot move data between them. This commonly happens when writing to os.tmpdir() and then renaming onto a project directory, or between two Docker volumes; copy the file and delete the original, or create your temp file inside the destination directory.
Does fs.rename overwrite an existing file?
Yes — if newPath is an existing file it is replaced silently, with no error and no flag to prevent it. If you need fail-if-exists behaviour, open the destination with the 'wx' flag first and handle EEXIST, or check with fs.access() while accepting the inherent race.
More examples
Publish a file atomically
The output shows config.json holding the complete document and the temp file no longer present. Because the swap is one syscall, a reader opening config.json at any instant gets either the previous version or the full new one — never an empty or partially written file. Keeping the temp file in the same directory also guarantees the rename stays on one filesystem.
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
const tmp = 'config.json.tmp';
// Write to a temp name in the SAME directory, then swap it in.
await fs.writeFile(tmp, JSON.stringify({ port: 8080 }));
await fs.rename(tmp, 'config.json');
console.log('published:', await fs.readFile('config.json', 'utf8'));
console.log('temp gone:', !existsSync(tmp));
await fs.unlink('config.json');Move a directory, and what happens without a parent
The first log lists logo.txt under the new path, proving the whole tree moved without any per-file work. The second logs ENOENT: rename never creates intermediate directories, so you must mkdir the destination's parent first. Both cases are the same syscall, which is why a directory move is as cheap as a file move.
import fs from 'node:fs/promises';
await fs.mkdir('v1/assets', { recursive: true });
await fs.writeFile('v1/assets/logo.txt', 'svg');
// One call moves the entire subtree.
await fs.rename('v1', 'v2');
console.log('moved subtree:', await fs.readdir('v2/assets'));
// The destination's parent must already exist.
try {
await fs.rename('v2', 'missing-parent/v3');
} catch (err) {
console.log('into a missing parent ->', err.code);
}
await fs.rm('v2', { recursive: true, force: true });
Discussion