The File System
Learn how to read and write files in Node.js using the fs module — and why doing it asynchronously keeps your server fast.
One of the first superpowers Node.js gives you that a browser never could is direct access to the file system. Your server can open files, read their contents, write new data, and delete things — all from JavaScript. This is how servers save uploaded photos, write log files, read configuration, and persist data without a full database. The module that makes all of this possible is called `fs` — short for File System — and it ships with every installation of Node.js, no npm install required.
Bring it in with a single line at the top of your file:
// CommonJS style — works in all Node.js versions
const fs = require('fs');
// Access the Promise-based API (we'll use this a lot)
const fsPromises = require('fs').promises;#Sync vs Async: Why It Matters
A coffee shop, not a DMV
Imagine a coffee shop with one barista. The synchronous approach: the barista takes your order, then stands frozen until your espresso finishes pulling — refusing to help anyone else in line. The asynchronous approach: the barista starts the machine, immediately takes the next order, and hands the espresso over when the timer dings. Node.js runs on a single thread, just like that one barista — async keeps the line flowing.
Sync is fine at startup — dangerous at runtime
Using readFileSync or writeFileSync before your server starts is fine — nothing is waiting on you yet. But calling them inside a request handler means every concurrent request stalls while one user's file loads. On a busy server, this causes a pile-up. Always reach for async inside handlers.
#Reading Files
There are three styles for reading a file. The sync style is a blocking one-liner. The callback style is the classic async approach — you pass a function that Node calls when the file is ready. The promises style is the modern favourite: it pairs with async/await and reads almost like synchronous code while remaining fully non-blocking.
const fs = require('fs');
const fsP = require('fs').promises;
// 1. Sync (blocks the event loop)
const data = fs.readFileSync('notes.txt', 'utf8');
// 2. Callback (non-blocking, classic style)
fs.readFile('notes.txt', 'utf8', (err, data) => {
if (err) return console.error(err.message);
console.log(data);
});
// 3. Promises + async/await (non-blocking, modern style)
async function readNote() {
const data = await fsP.readFile('notes.txt', 'utf8');
console.log(data);
}#Writing and Appending Files
const fs = require('fs').promises;
const path = require('path');
const logFile = path.join(__dirname, 'app.log');
async function logEvent(message) {
const line = `[${new Date().toISOString()}] ${message}\n`;
// appendFile adds to the end; writeFile replaces the whole file
await fs.appendFile(logFile, line, 'utf8');
console.log('Logged:', message);
}
logEvent('Server started');#Getting Paths Right with path.join
const fs = require('fs').promises;
const path = require('path');
// __dirname is always the folder this file lives in
const configPath = path.join(__dirname, 'config.json');
async function loadConfig() {
try {
const raw = await fs.readFile(configPath, 'utf8');
return JSON.parse(raw);
} catch (err) {
console.error('Config not found, using defaults.');
return { port: 3000 };
}
}
loadConfig().then(cfg => console.log('Port:', cfg.port));You call `fs.readFile` inside an HTTP request handler. What is the main advantage over using `fs.readFileSync`?
const fs = require('fs').promises;
const path = require('path');
const dataFile = path.join(__dirname, 'users.json');
// Real pattern: read the file, modify the data, write it back
async function addUser(name) {
let users = [];
try {
const raw = await fs.readFile(dataFile, 'utf8');
users = JSON.parse(raw);
} catch (_) {
// File doesn't exist yet — start with an empty list
}
users.push({ id: users.length + 1, name });
await fs.writeFile(dataFile, JSON.stringify(users, null, 2), 'utf8');
console.log(`Saved ${users.length} user(s).`);
}
addUser('Ada');Key takeaways
- The built-in `fs` module gives Node.js full access to the file system — no install needed.
- Sync methods (readFileSync, writeFileSync) block the event loop — fine at startup, dangerous inside request handlers.
- `fs.promises` with async/await is the modern, readable, non-blocking approach — prefer it.
- Use `path.join(__dirname, 'filename')` to build reliable absolute paths that work on any OS.
- Wrap all file reads in try/catch (or an `err` check) — files can be missing, locked, or unreadable.
Both appendFile and writeFile run against the same file. What gets printed?
const fs = require('fs').promises;
async function run() {
await fs.writeFile('log.txt', 'A', 'utf8');
await fs.appendFile('log.txt', 'B', 'utf8');
await fs.writeFile('log.txt', 'C', 'utf8');
const data = await fs.readFile('log.txt', 'utf8');
console.log(data);
}
run();This code runs inside an HTTP request handler on a busy server. What's the problem?
const fs = require('fs');
function handleRequest(req, res) {
const data = fs.readFileSync('notes.txt', 'utf8');
res.end(data);
}Build a safe, cross-platform absolute path to 'config.json' in the same folder as this file.
const path = require('path'); const configPath = path.join(, 'config.json');
Order these lines into the classic read-modify-write cycle: read the file, parse it, change the data, then write it back.
const users = JSON.parse(raw);
await fs.writeFile(dataFile, JSON.stringify(users, null, 2), 'utf8');
const raw = await fs.readFile(dataFile, 'utf8');
users.push({ id: users.length + 1, name });Write an async function called saveNote(title, content) that: (1) builds a safe absolute path to a file called note.txt in the same directory as the script using path.join and __dirname, (2) formats a string like "Title: <title>\nContent: <content>\n", (3) writes that string to the file using fs.promises.writeFile, and (4) logs 'Note saved!' when done. Call it with a title and content of your choice.
Try it yourself — a starting point to build on:
const fs = require('fs').promises;
const path = require('path');
// TODO: Build an absolute path to 'note.txt' in this directory
const notePath = /* your code here */;
async function saveNote(title, content) {
// TODO: Format the note text
const text = /* your code here */;
// TODO: Write text to notePath
await /* your code here */;
console.log('Note saved!');
}
// Call your function
saveNote('Shopping List', 'Eggs, milk, coffee');