fs and fs/promises
node:fs and node:fs/promises read, write, and stream files on disk - promise APIs for async work, streams for scale, and explicit flags for security-sensitive reads and writes.
Search across all documentation pages
node:fs and node:fs/promises read, write, and stream files on disk - promise APIs for async work, streams for scale, and explicit flags for security-sensitive reads and writes.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
await mkdir('data', { recursive: true });
await writeFile('data/out.json', JSON.stringify({ ok: true }));
const text = await readFile('data/out.json', 'utf8');When to reach for this:
import { open, writeFile, rename } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
import { createWriteStream } from 'node:fs';
async function atomicWrite(path: string, data: string): Promise<void> {
const tmp = `${path}.tmp`;
await writeFile(tmp, data, { encoding: 'utf8', mode: 0o600 });
await rename(tmp, path);
}
async function tailBytes(path: string, max: number): Promise<Buffer> {
const handle = await open(path, 'r');
try {
const stat = await handle.stat();
const size = stat.size;
const start = Math.max(0, size - max);
const len = size - start;
const buf = Buffer.alloc(len);
await handle.read(buf, 0, len, start);
return buf;
} finally {
await handle.close();
}
}
await pipeline(createReadStream('large.log'), createGzip(), createWriteStream('large.log.gz'));What this demonstrates:
filehandle.read reads tail segment without loading whole filepipeline + createReadStream compresses large logs with bounded memory0o600 for sensitive config on Unixopen/filehandle pattern for positioned reads/writes.'r', 'w', 'a', 'wx' exclusive create - prevent accidental overwrite.fs.watch / fs.promises.watch - debounce in dev tools; prefer explicit reload signals in prod.| Scenario | API |
|---|---|
| Small config | readFile |
| Large download | createReadStream |
| Atomic config | write temp + rename |
| Directory ensure | mkdir({ recursive: true }) |
import { access, constants } from 'node:fs/promises';
await access('/path/to/file', constants.R_OK);fs/promises or streams.../../etc/passwd. Fix: resolve under base directory.try/finally with close().| Alternative | Use When | Don't Use When |
|---|---|---|
| S3 SDK | Durable object storage | Local temp scratch OK |
fs-extra npm | Recursive copy helpers | Built-in recursive mkdir enough |
| Database BLOB | Queryable artifacts | Large static assets |
| memfs (tests) | Unit tests without disk | Production |
Use node:fs/promises - promisify legacy only in brownfield.
File larger than memory budget or piping to HTTP/gzip - always streams for uploads/downloads scale.
Exclusive write - fails if file exists - useful for lock files.
Same filesystem atomic on POSIX - use for config swaps; cross-device rename may need copy+delete.
Race between stat and open - open with O_NOFOLLOW patterns for security-sensitive code.
Permission errors - run container as non-root with correct volume mounts.
import { tmpdir } from 'node:os' - clean up temp files in finally block.
Node 20+ readdir with recursive: true - mind result size on large trees.
fs.copyFile for simple copies - streams for transform while copying.
Ensure UID GID matches node user for write permissions.
path module handles - avoid hard-coded backslashes.
Streams Basics for pipeline patterns.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026