NodeJSDeleting & Renaming Files

Deleting & Renaming Files

Removing files (unlink, rm), renaming and moving them (rename), and copying (copyFile) round out the basic filesystem toolkit. These operations are destructive, so the emphasis here is on doing them safely — and on the platform quirks that bite people moving files between drives.

Deleting a file

JS
import { unlink } from 'node:fs/promises'

await unlink('temp.txt')        // delete a single file

// Handle the "already gone" case gracefully:
try {
  await unlink('maybe-gone.txt')
} catch (err) {
  if (err.code !== 'ENOENT') throw err   // ignore not-found, re-throw the rest
}
`unlink` is for files; use `rm`/`rmdir` for directories
`unlink` removes a single file (or symlink) and throws `EISDIR` if you point it at a directory. To delete directories — empty or not — use `rm` with options (below) or `rmdir` for empty ones. There's no "recycle bin": deletes are immediate and permanent.
Deleting directories and trees

The modern rm (Node 14.14+) handles files and directory trees, with options that make it forgiving:

JS
import { rm } from 'node:fs/promises'

await rm('build', { recursive: true, force: true })   // like 'rm -rf build'
// recursive: delete contents too · force: don't error if it's missing

await rm('single.txt')                                 // a single file works too
`{ recursive: true, force: true }` is `rm -rf` — be certain of the path
This deletes an entire tree with no confirmation and no recovery. A bug that builds the path wrong — an empty variable making the path `/` or the project root — can wipe out far more than intended. Validate the path, never interpolate unchecked user input into it, and consider an allow-list of deletable directories.
Renaming and moving

rename both renames and moves — moving is just renaming to a path in another directory. It's atomic on the same filesystem:

JS
import { rename } from 'node:fs/promises'

await rename('draft.txt', 'final.txt')              // rename in place
await rename('final.txt', 'archive/final.txt')      // move (same filesystem)
`rename` fails across filesystems with EXDEV
A `rename` between different devices/partitions (e.g. `/tmp` on one volume to `/data` on another, or across a Docker mount) throws `EXDEV: cross-device link not permitted` — because an atomic rename can't span filesystems. The fix is **copy then delete**: `copyFile` to the destination, then `unlink` the source. `fs.cp` can also move trees.

Cross-device-safe move

JS
import { rename, copyFile, unlink } from 'node:fs/promises'

async function move(src, dest) {
  try {
    await rename(src, dest)              // fast path: same filesystem
  } catch (err) {
    if (err.code !== 'EXDEV') throw err
    await copyFile(src, dest)            // fallback: copy across devices
    await unlink(src)                    // then remove the original
  }
}
Copying files

JS
import { copyFile, constants } from 'node:fs/promises'

await copyFile('a.txt', 'b.txt')                              // overwrite if exists
await copyFile('a.txt', 'b.txt', constants.COPYFILE_EXCL)    // fail if b exists
`fs.cp` copies whole directory trees
`copyFile` handles one file. To copy a directory recursively, use `fs.cp(src, dest, { recursive: true })` (Node 16.7+) — the programmatic equivalent of `cp -r`. It also supports filters and preserving timestamps.
Check-then-act is a race condition
Don't `exists()` then act — just act and catch
Checking "does this file exist?" before deleting/renaming is a **TOCTOU** (time-of-check-to-time-of-use) race: the file can vanish or appear in the gap between the check and the operation. The robust pattern is to *attempt* the operation and handle the error (`ENOENT`, `EEXIST`). This is also why `fs.exists` is deprecated — it encourages the buggy pattern.

JS
// ✗ Racy
if (await exists('f.txt')) await unlink('f.txt')   // f.txt may vanish between!

// ✓ Robust — act, then handle the outcome
try { await unlink('f.txt') }
catch (err) { if (err.code !== 'ENOENT') throw err }
Operation reference

Goal

Function

Delete a file

unlink(path)

Delete a tree (rm -rf)

rm(path, { recursive: true, force: true })

Delete an empty dir

rmdir(path)

Rename / move (same FS)

rename(src, dest)

Move across filesystems

copyFile + unlink (or fs.cp)

Copy a file

copyFile(src, dest)

Copy a tree

cp(src, dest, { recursive: true })

Next
Create, list, and traverse directories: [Working with Directories](/nodejs/working-with-directories).