The fs module — synchronous, callback, and promise-based APIs
Node's filesystem module offers the exact same operation in three different forms — and picking the wrong one, specifically the synchronous form inside a server, is a real, common way to accidentally block the entire event loop this domain's earlier lessons explained.
3 min read
Three ways to read the same file
const fs = require("node:fs");
const fsPromises = require("node:fs/promises");
// Synchronous — BLOCKS the entire thread until the read completes
const data1 = fs.readFileSync("./config.json", "utf8");
// Callback-based — the original async API, non-blocking
fs.readFile("./config.json", "utf8", (err, data2) => { /* ... */ });
// Promise-based — modern, works cleanly with async/await
const data3 = await fsPromises.readFile("./config.json", "utf8");fs offers the exact same set of operations in three genuinely different execution models: the Sync suffix (readFileSync) blocks the calling thread entirely until the operation finishes, the plain callback form (readFile) is non-blocking and calls back once the result is ready, and fs/promises wraps the same non-blocking behavior in a Promise-returning API that pairs cleanly with async/await. All three touch the disk the same way underneath — the difference is entirely about what happens to the rest of the program while that disk I/O is in progress.
Why the synchronous version is a real, specific danger inside a server
const http = require("node:http");
const fs = require("node:fs");
http.createServer((req, res) => {
const data = fs.readFileSync("./large-file.txt"); // BLOCKS the entire server for EVERY request
res.end(data);
}).listen(3000);Because the earlier non-blocking-I/O lesson established that Node handles many concurrent requests on a single thread specifically by never blocking that thread, calling readFileSync inside a request handler defeats the entire model directly: while one request's file read is happening synchronously, the single thread genuinely cannot do anything else — not even start processing a second incoming request — until that read finishes. This is a real, common mistake, especially for developers coming from a synchronous/blocking language, and it's the exact bug the async fs.readFile/fs/promises forms exist to prevent.
Existence checks and the TOCTOU race
// A tempting but genuinely unsafe pattern
if (fs.existsSync("./file.txt")) {
const data = fs.readFileSync("./file.txt"); // the file could be DELETED between these two lines
}
// The safer pattern — attempt the operation, handle the failure directly
try {
const data = await fsPromises.readFile("./file.txt");
} catch (err) {
if (err.code === "ENOENT") { /* handle "file doesn't exist" here, at the point it actually matters */ }
}Checking whether a file exists and then acting on it in a separate step has a real, if narrow, race condition (called TOCTOU — "time of check to time of use"): something else (another process, a concurrent request in the same server) could delete or modify the file in the gap between the check and the actual read. The more robust pattern is attempting the operation directly and handling the specific error it produces — ENOENT for "no such file" is a real, standard Node error code — rather than pre-checking and hoping nothing changes in between.
Directory operations follow the same three-form pattern
await fsPromises.mkdir("./uploads", { recursive: true }); // recursive: true creates parent dirs too, no error if it already exists
const files = await fsPromises.readdir("./uploads"); // returns an array of filenames
await fsPromises.rm("./uploads/temp.txt"); // deletes a file
await fsPromises.rm("./uploads/old-folder", { recursive: true }); // deletes a directory and everything in itDirectory operations (mkdir, readdir, rm, rename) follow the exact same synchronous/callback/promise three-form pattern as file operations, with the same underlying trade-off — the async forms are the right default for anything running inside a server, and the recursive option on mkdir (create any missing parent directories) and rm (delete a directory's entire contents) are common, practical options worth knowing explicitly rather than rediscovering through a thrown error.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is calling `fs.readFileSync()` inside an HTTP request handler a real, specific danger?
2. What is the TOCTOU race condition in the context of checking `fs.existsSync()` before reading a file?
3. What's the practical difference between the three forms of fs operations (Sync, callback, promises)?