Node ships the same operations three times over. node:fs/promises returns promises, node:fs exports error-first callback functions, and the same node:fs module exports blocking ...Sync variants. Each has a job.
import { readFile } from 'node:fs/promises';
import { readFile as readFileCb, readFileSync } from 'node:fs';
const text = await readFile('package.json', 'utf8'); // 1. promise flavor
console.log('promise :', JSON.parse(text).name);
readFileCb('package.json', 'utf8', (err, data) => { // 2. error-first callback
if (err) throw err;
console.log('callback:', JSON.parse(data).name);
});
const raw = readFileSync('package.json', 'utf8'); // 3. blocking
console.log('sync :', JSON.parse(raw).name);promise : demo sync : demo callback: demo
Look at the order. The synchronous line is written last and prints second, because it never yields; the callback was registered first and prints last, because it waits for a turn of the event loop.
Use promises by default. Use callbacks in a measured hot path, where they save a promise per call, or inside code that already speaks callbacks — streams and older libraries. Use the synchronous flavor only before your server starts listening, in build tools and in tests, never in a request handler, where it freezes every connection your process is holding.
Whichever flavor you pick, keep fs.exists() out of it — the module's one formally deprecated member (Stability 0), because its callback has no error argument. Ask for the operation you want and handle the failure.