Reading and Writing Files

readFile and writeFile handle whole files: Node opens the file, moves its bytes, and closes it. Pass an encoding and you get a string; leave it off and you get a Buffer, which is what you want for images and archives.

Whole-file reads and writes, flags, and a FileHandleJavaScript
import { writeFile, appendFile, readFile, open, rm } from 'node:fs/promises';
const file = 'notes.txt';
await writeFile(file, 'first line\n', 'utf8');   // creates or truncates
await appendFile(file, 'second line\n');         // utf8 is the default
console.log('text  :', JSON.stringify(await readFile(file, 'utf8')));
try {
  await writeFile(file, 'clobbered', { flag: 'wx' });   // 'x' = fail if it exists
} catch (err) {
  console.log('flag  :', err.code, '-', err.syscall, 'refused to truncate');
}
const handle = await open(file, 'r');            // one descriptor, many reads
try {
  const { bytesRead, buffer } = await handle.read({ length: 4, position: 6 });
  console.log('handle:', (await handle.stat()).size, 'bytes, read',
              buffer.subarray(0, bytesRead) + '');
} finally {
  await handle.close();
}
await rm(file, { force: true });
Output
text  : "first line\nsecond line\n"
flag  : EEXIST - open refused to truncate
handle: 23 bytes, read line

The flag option takes C's open flags: 'w' truncates, 'a' appends, 'r' reads, and adding x turns "create it" into "create it or fail." 'wx' claims a lock file without a check-then-write race, because the kernel enforces the exclusivity, not your code.

open() returns a FileHandle when you need several operations on one file — read a header, seek, read a record. Every handle must be closed, and try/finally guarantees it: a leaked descriptor lives until the process exits, and EMFILE errors under load are miserable to diagnose.

Three more operations finish the set. rename(from, to) moves a file, atomically within one volume. cp(src, dest, { recursive: true }) copies a tree; it lost its experimental label in Node 22.3.0. rm(path, { recursive: true, force: true }) is the rm -rf equivalent — and rmdir's own recursive option, deprecated since Node 14.14.0, was removed in Node 25, so rm is now the only built-in way to delete a non-empty directory.