Files and paths
Choose a filesystem API style
Choose between synchronous, callback, and promise-based filesystem operations according to where the code runs.
Node exposes synchronous, callback, and promise-based filesystem APIs. Pick the style based on where the code runs.
The three forms behave differently when they finish and when they fail:
import { readFileSync, readFile } from 'node:fs'
import { readFile as readFilePromise } from 'node:fs/promises'
const text = readFileSync('note.txt', 'utf8')
readFile('note.txt', 'utf8', (error, value) => {
if (error) return console.error(error)
console.log(value)
})
const value = await readFilePromise('note.txt', 'utf8')
The synchronous call blocks the event-loop thread until the filesystem work finishes. It throws on failure. That is fine for a tiny one-shot CLI or reading one config file before a server starts listening. Inside a request handler it freezes every other client.
Callback and promise operations run asynchronously, usually on Node’s worker pool. The callback form passes the error first. The promise form rejects and pairs naturally with await and try/catch.
I reach for node:fs/promises in most application code. Callback APIs can use less allocation and might matter in a measured hot path. Do not pick them just because they look lower level.
Asynchronous does not mean synchronized. Two concurrent writes to the same file can still race, whether you use callbacks or promises. Serialize related changes or use storage built for concurrent writers.
For very large files, use streams instead of reading everything into memory.
Streams process data in chunks and respect backpressure when the reader is slower than the writer. They do not make CPU-heavy work non-blocking by themselves.
A missing file throws or rejects with ENOENT in all three styles. The difference is timing: sync throws immediately, callbacks pass the error as the first argument, promises reject.
Try this on your own machine: read the same missing file with all three API styles and note how each reports the error. Then put the synchronous version inside a timer-driven demo and watch which scheduled callback gets delayed.
Lesson completed