Node.js and Express course menu
Node.js File System (fs): Read, Write and Manage Files
Work with files in Node.js using fs/promises: reading and writing text and JSON files, appending, checking whether files exist, handling ENOENT errors, creating and listing folders, file information with stat, and why to avoid synchronous methods in servers.
On this page
The fs (file system) module reads and writes files and folders. Use the promise version, node:fs/promises, with async/await: it does not block the program while the disk works.
Write and read a text file#
import { readFile, writeFile } from "node:fs/promises";
await writeFile("notes.txt", "Buy milk\nCall the bank\n");
const text = await readFile("notes.txt", "utf8");
console.log(text.trim().split("\n"));
console.log("Characters:", text.length);node notes.js[ 'Buy milk', 'Call the bank' ]
Characters: 23writeFilecreates the file, or replaces it if it exists.- Pass
"utf8"toreadFileto get a string; without an encoding you get aBufferof raw bytes. - Top-level
awaitworks in ES modules.
Append to a file#
import { appendFile, readFile } from "node:fs/promises";
for (const event of ["server started", "user signed in", "order placed"]) {
await appendFile("app.log", `${event}\n`);
}
console.log(await readFile("app.log", "utf8"));node log.jsserver started
user signed in
order placedJSON files#
JSON files are a simple way to store small amounts of data: parse after reading, stringify before writing.
[
{ "id": 1, "name": "Notebook", "price": 60, "stock": 120 },
{ "id": 2, "name": "Gel pen", "price": 25, "stock": 0 },
{ "id": 3, "name": "Backpack", "price": 1299, "stock": 8 }
]import { readFile, writeFile } from "node:fs/promises";
const products = JSON.parse(await readFile("products.json", "utf8"));
const outOfStock = products.filter((p) => p.stock === 0).map((p) => p.name);
console.log("Out of stock:", outOfStock);
const updated = products.map((p) => (p.stock === 0 ? { ...p, stock: 50 } : p));
await writeFile("products.json", JSON.stringify(updated, null, 2));
console.log("Saved. Gel pen stock is now", updated[1].stock);node restock.jsOut of stock: [ 'Gel pen' ]
Saved. Gel pen stock is now 50JSON.stringify(data, null, 2) writes indented, readable JSON. For anything bigger than a small config or demo, use a database: a JSON file is rewritten completely on every save and can be corrupted if two requests write at once.
Handling missing files#
Reading a file that does not exist throws an error with code: "ENOENT" ("error, no entry"). Catch it and decide what to do:
import { readFile } from "node:fs/promises";
async function loadSettings() {
try {
return JSON.parse(await readFile("settings.json", "utf8"));
} catch (err) {
if (err.code === "ENOENT") return { theme: "light", language: "en" }; // defaults
throw err; // other errors are real problems
}
}
console.log(await loadSettings());node settings.js{ theme: 'light', language: 'en' }Common error codes: ENOENT (not found), EACCES (permission denied), EEXIST (already exists), EISDIR (expected a file, found a folder). Prefer trying the operation and handling the error over checking first with access: the file could change between the check and the use.
Folders#
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
await mkdir("reports/2026/october", { recursive: true }); // creates parent folders too
await writeFile("reports/2026/october/sales.csv", "day,amount\n1,4500\n2,5200\n");
await writeFile("reports/2026/summary.txt", "October: 9700");
console.log(await readdir("reports/2026"));
const info = await stat("reports/2026/october/sales.csv");
console.log("Size in bytes:", info.size, "| is a file:", info.isFile());
const entries = await readdir("reports", { recursive: true });
console.log(entries.length, "entries inside reports");
await rm("reports", { recursive: true, force: true }); // delete the folder and its contents
console.log("Deleted reports/");node folders.js[ 'october', 'summary.txt' ]
Size in bytes: 25 | is a file: true
4 entries inside reports
Deleted reports/readdir lists names (order is not guaranteed on every system, so sort if it matters). rm with recursive: true deletes a folder and everything in it: use it with care.
Synchronous and callback versions#
fs also has synchronous methods (readFileSync) and callback-style ones (fs.readFile(path, callback)):
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("config.json", "utf8"));Synchronous calls block the whole program until the disk responds. That is fine in short scripts and at start-up (reading config once), but in a server it stops every other request from being handled. Use fs/promises in request handlers. Callback APIs appear in older code; promises with await are easier to read.
Common mistakes#
Practice#
Practice exercise
Write todo.js that keeps tasks in tasks.json: if the file does not exist, start with an empty list; add the task "Water the plants"; save the file; and print how many tasks are saved. Running it twice should report 1 and then 2.
Show hint
Use the ENOENT pattern to default to [], push the new task, then writeFile with JSON.stringify(tasks, null, 2).
Show solution
import { readFile, writeFile } from "node:fs/promises";
async function loadTasks() {
try {
return JSON.parse(await readFile("tasks.json", "utf8"));
} catch (err) {
if (err.code === "ENOENT") return [];
throw err;
}
}
const tasks = await loadTasks();
tasks.push({ text: "Water the plants", done: false });
await writeFile("tasks.json", JSON.stringify(tasks, null, 2));
console.log(`${tasks.length} task(s) saved`);node todo.js
node todo.js1 task(s) saved
2 task(s) savedKey takeaway#
Next: Paths and URLs.
Finished this lesson?