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.
Recipe
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:
- Loading config at startup (small JSON)
- Streaming CSV export to HTTP response
- Writing uploaded temp files before virus scan
- Rotating log files to disk in workers
Working Example
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:
- Atomic write via temp file + rename reduces partial-write corruption
filehandle.readreads tail segment without loading whole filepipeline+createReadStreamcompresses large logs with bounded memory- File mode
0o600for sensitive config on Unix
Deep Dive
How It Works
- libuv thread pool runs some fs operations - heavy sync fs still blocks loop if sync API used.
- File descriptors -
open/filehandlepattern for positioned reads/writes. - Flags -
'r','w','a','wx'exclusive create - prevent accidental overwrite. - watch -
fs.watch/fs.promises.watch- debounce in dev tools; prefer explicit reload signals in prod.
API Choice
| Scenario | API |
|---|---|
| Small config | readFile |
| Large download | createReadStream |
| Atomic config | write temp + rename |
| Directory ensure | mkdir({ recursive: true }) |
TypeScript Notes
import { access, constants } from 'node:fs/promises';
await access('/path/to/file', constants.R_OK);Gotchas
- readFileSync in HTTP handler - blocks event loop. Fix:
fs/promisesor streams. - Path traversal - user supplies
../../etc/passwd. Fix: resolve under base directory. - Watching thousands of files - EMFILE and CPU. Fix: narrow watch scope or use build tool.
- Default UTF-8 on binary files - corruption. Fix: omit encoding for Buffer.
- Not closing file handles - EMFILE leaks. Fix:
try/finallywithclose().
Alternatives
| 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 |
FAQs
promises vs callbacks fs?
Use node:fs/promises - promisify legacy only in brownfield.
When createReadStream?
File larger than memory budget or piping to HTTP/gzip - always streams for uploads/downloads scale.
What is flag wx?
Exclusive write - fails if file exists - useful for lock files.
rename atomicity?
Same filesystem atomic on POSIX - use for config swaps; cross-device rename may need copy+delete.
fs.stat and TOCTOU?
Race between stat and open - open with O_NOFOLLOW patterns for security-sensitive code.
EPERM EACCES?
Permission errors - run container as non-root with correct volume mounts.
tmpdir for uploads?
import { tmpdir } from 'node:os' - clean up temp files in finally block.
readdir recursive?
Node 20+ readdir with recursive: true - mind result size on large trees.
copyFile?
fs.copyFile for simple copies - streams for transform while copying.
Docker volumes?
Ensure UID GID matches node user for write permissions.
Windows paths?
path module handles - avoid hard-coded backslashes.
relation to streams doc?
Streams Basics for pipeline patterns.
Related
- path and url - safe path join
- Streams Basics - createReadStream
- Buffer Security - file permissions
- Built-in APIs Basics - overview
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.