Sync vs Async vs Promises API
Every fs operation comes in three forms: synchronous (readFileSync), callback (readFile(…, cb)), and promise (fs/promises). They do the same work but have very different consequences for your program's responsiveness. Choosing correctly is one of the most impactful decisions in a Node app.
The same read, three ways
// SYNC — blocks the thread until done
import { readFileSync } from 'node:fs'
const a = readFileSync('f.txt', 'utf8')
// CALLBACK — non-blocking, error-first callback
import { readFile } from 'node:fs'
readFile('f.txt', 'utf8', (err, b) => { if (err) throw err; use(b) })
// PROMISE — non-blocking, awaitable ← preferred
import { readFile as readFileP } from 'node:fs/promises'
const c = await readFileP('f.txt', 'utf8')Side-by-side comparison
Sync | Callback | Promises | |
|---|---|---|---|
Blocks event loop | Yes | No | No |
Error handling |
| Error-first arg |
|
Composes / chains | Trivially (but blocks) | Awkward (nesting) | Cleanly |
Return value | Direct | Via callback | Via promise |
Best for | Scripts, startup | Legacy, streams | App code |
Why sync blocks everything
import { readFileSync } from 'node:fs'
// In a request handler this is poison — every other request waits:
app.get('/report', (req, res) => {
const data = readFileSync('huge-report.csv', 'utf8') // ✗ blocks the loop
res.send(data)
})When sync is actually fine
import { readFileSync } from 'node:fs'
// Startup config — runs once, nothing to block. Sync is fine and readable:
const config = JSON.parse(readFileSync('./config.json', 'utf8'))
const cert = readFileSync('./server.key')
startServer(config, cert)Callback vs promises
Both are non-blocking; promises just read better and compose without nesting. Prefer fs/promises unless you have a specific reason (a tight streaming loop, or interop with callback-based code):
// Callback — nests as steps grow
readFile('a.txt', 'utf8', (err, a) => {
if (err) return done(err)
writeFile('b.txt', a.toUpperCase(), (err) => {
if (err) return done(err)
done(null)
})
})
// Promises — flat and try/catch-able
try {
const a = await readFileP('a.txt', 'utf8')
await writeFileP('b.txt', a.toUpperCase())
} catch (err) { done(err) }The fs/promises advantage
Reads top-to-bottom like synchronous code, but never blocks the loop.
One
try/catchcovers a whole sequence — no repeated error-first checks.Composes with
Promise.all,for await, timeouts, and the patterns from Async Control-Flow Patterns.Works with
AbortSignalfor cancellation:readFile(path, { signal }).
Decision guide
Context | Use |
|---|---|
Server request handlers |
|
App initialization / startup | Sync is fine (runs once) |
CLI tools & build scripts | Sync is fine (no concurrency) |
Streaming large data | Streams ( |
Bridging old callback libraries | Callback API or |