Topic 6: File System
📖 7 min read · 🎯 intermediate · 🧭 Prerequisites: building-a-http-server-with-node-js-using-http-apis, json-data
Why this matters
Here's the thing — almost every real app you build will need to touch files. Reading a config file, writing logs, saving a user's uploaded photo, checking if a folder exists. You'll hit these moments constantly. And in Node.js, the tool that handles all of it is the built-in fs module. No install needed, no setup — it's right there waiting. Once you understand fs, you stop thinking of your app as just code running in memory and start seeing it as something that can actually read from and write to your computer's file system. That shift matters. Let's dig in.
What You'll Learn
- Import and use the built-in
fsmodule for file operations - Read, write, append, and delete files with both synchronous and asynchronous APIs
- Create, read, and delete directories
- Watch files and directories for live changes
- Use readable and writable streams to handle large files efficiently
The Analogy
Think of the fs module as Vizag's city clerk office. Each file is a physical document stored in a cabinet, and each directory is a labelled drawer. When you call readFile, a clerk walks to the cabinet, retrieves the document, and hands it back to you — asynchronously, so you can keep doing other work while you wait. writeFile is the clerk typing up a new document and filing it. appendFile is the clerk stapling a note to the bottom of an existing one. unlink shreds a document permanently. Streams are the rolling mail-cart the clerk uses when a document is too large to carry in one trip — content rolls in chunk by chunk, keeping the hallways clear.
Chapter 1: Importing the fs Module
The fs module ships with Node.js — no npm install required. A single require call puts the entire file-system API at your fingertips.
const fs = require('fs');
Every example in this lesson begins with this line. From here you can read, write, update, delete, watch, and stream files and directories using both asynchronous (non-blocking, callback-based) and synchronous (blocking) variants of each operation.
Chapter 2: Reading Files
Node.js offers two reading styles. The asynchronous version is preferred in production because it does not block the event loop.
Asynchronous file reading — readFileAsync.js
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Synchronous file reading — readFileSync.js
const fs = require('fs');
try {
const data = fs.readFileSync('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
Async (readFile) | Sync (readFileSync) | |
|---|---|---|
| Blocks event loop? | No | Yes |
| Best for | Servers, concurrent I/O | CLI scripts, startup config |
| Error handling | Callback err argument | try / catch |
Chapter 3: Writing Files
writeFile (and its sync twin) creates or overwrites the target file with the supplied content.
Asynchronous file writing — writeFileAsync.js
const fs = require('fs');
const content = 'Hello, Vizag!';
fs.writeFile('example.txt', content, 'utf8', (err) => {
if (err) {
console.error(err);
return;
}
console.log('File has been saved.');
});
Synchronous file writing — writeFileSync.js
const fs = require('fs');
const content = 'Hello, Vizag!';
try {
fs.writeFileSync('example.txt', content, 'utf8');
console.log('File has been saved.');
} catch (err) {
console.error(err);
}
If
example.txtalready exists, both calls will replace its entire contents. To add data without destroying existing content, useappendFileinstead.
Chapter 4: Updating Files (Appending)
appendFile adds data to the end of an existing file. If the file does not exist it is created automatically.
Asynchronous file appending — appendFileAsync.js
const fs = require('fs');
const additionalContent = '\nWelcome to Vizag!';
fs.appendFile('example.txt', additionalContent, 'utf8', (err) => {
if (err) {
console.error(err);
return;
}
console.log('Content has been appended.');
});
Synchronous file appending — appendFileSync.js
const fs = require('fs');
const additionalContent = '\nWelcome to Vizag!';
try {
fs.appendFileSync('example.txt', additionalContent, 'utf8');
console.log('Content has been appended.');
} catch (err) {
console.error(err);
}
The \n at the start of additionalContent ensures the new text begins on its own line rather than being glued to the end of the previous content.
Chapter 5: Deleting Files
fs.unlink permanently removes a file. There is no recycle bin — once unlinked, the file is gone.
Asynchronous file deletion — deleteFileAsync.js
const fs = require('fs');
fs.unlink('example.txt', (err) => {
if (err) {
console.error(err);
return;
}
console.log('File has been deleted.');
});
Synchronous file deletion — deleteFileSync.js
const fs = require('fs');
try {
fs.unlinkSync('example.txt');
console.log('File has been deleted.');
} catch (err) {
console.error(err);
}
Always guard delete operations behind a check (or a try/catch) — calling unlink on a path that does not exist returns an ENOENT error.
Chapter 6: Working with Directories
Directories are containers, but the fs API treats them similarly to files: create, read, and delete — all with async and sync variants.
Creating a directory — createDirectory.js
const fs = require('fs');
const dirPath = './newDir';
// Asynchronous
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) {
console.error(err);
return;
}
console.log('Directory created.');
});
// Synchronous
try {
fs.mkdirSync(dirPath, { recursive: true });
console.log('Directory created.');
} catch (err) {
console.error(err);
}
The { recursive: true } option mirrors mkdir -p on the command line — it creates all intermediate directories and does not throw if the directory already exists.
Reading a directory — readDirectory.js
const fs = require('fs');
const dirPath = './';
fs.readdir(dirPath, (err, files) => {
if (err) {
console.error(err);
return;
}
console.log('Directory contents:', files);
});
readdir returns an array of file/folder names (not full paths) inside the target directory.
Deleting a directory — deleteDirectory.js
const fs = require('fs');
const dirPath = './newDir';
// Asynchronous
fs.rmdir(dirPath, { recursive: true }, (err) => {
if (err) {
console.error(err);
return;
}
console.log('Directory deleted.');
});
// Synchronous
try {
fs.rmdirSync(dirPath, { recursive: true });
console.log('Directory deleted.');
} catch (err) {
console.error(err);
}
{ recursive: true } on rmdir removes the directory and all of its contents. Use with caution — this is the Node.js equivalent of rm -rf.
Chapter 7: Watching Files
fs.watch registers a listener that fires whenever a file or directory is modified. It is ideal for hot-reload tools and live-config systems.
watchFile.js
const fs = require('fs');
const filePath = 'example.txt';
fs.watch(filePath, (eventType, filename) => {
if (filename) {
console.log(`${filename} file changed with event type: ${eventType}`);
} else {
console.log('filename not provided');
}
});
The eventType argument will be either 'rename' (file was renamed or deleted) or 'change' (file contents changed). Note that filename can be null on some platforms, so the guard is important.
Chapter 8: File Streams
For large files, loading the entire content into memory at once is impractical. Streams solve this by processing data in small chunks as they flow through a pipe.
flowchart LR
A[Disk: largeFile.txt] -->|chunk 1| B[ReadStream]
B -->|chunk 2| C[Your callback / WriteStream]
C -->|chunk 3| D[Output / Disk]
Reading with streams — readStream.js
const fs = require('fs');
const readStream = fs.createReadStream('largeFile.txt', 'utf8');
readStream.on('data', (chunk) => {
console.log('New chunk received:', chunk);
});
readStream.on('end', () => {
console.log('File reading completed.');
});
readStream.on('error', (err) => {
console.error('Error reading file:', err);
});
data— fires for every chunk arriving from diskend— fires once when all chunks have been deliverederror— fires if the file cannot be opened or a read error occurs
Writing with streams — writeStream.js
const fs = require('fs');
const writeStream = fs.createWriteStream('largeFile.txt');
writeStream.write('This is the first line.\n');
writeStream.write('This is the second line.\n');
writeStream.end();
writeStream.on('finish', () => {
console.log('File writing completed.');
});
writeStream.on('error', (err) => {
console.error('Error writing file:', err);
});
Call writeStream.end() to signal that no more data will be written. The finish event fires after all data has been flushed to disk.
🧪 Try It Yourself
Task: Build a simple Node.js logger that appends a timestamped line to activity.log every time a target file changes.
- Create a file called
target.txtwith any content. - Create
logger.jsusing the starter snippet below. - Run
node logger.js, then in a separate terminal edit and savetarget.txt. - Open
activity.log— you should see a new line for each change event.
Success criterion: After two saves to target.txt, activity.log contains two timestamped entries.
const fs = require('fs');
const watched = 'target.txt';
const logFile = 'activity.log';
fs.watch(watched, (eventType, filename) => {
const entry = `[${new Date().toISOString()}] ${filename} — ${eventType}\n`;
fs.appendFile(logFile, entry, 'utf8', (err) => {
if (err) console.error('Log write failed:', err);
else console.log('Logged:', entry.trim());
});
});
console.log(`Watching ${watched} for changes...`);
🔍 Checkpoint Quiz
Q1. Why is fs.readFile generally preferred over fs.readFileSync in a Node.js server?
A) readFile is faster at the hardware level
B) readFile is non-blocking and allows the event loop to handle other requests while waiting for disk I/O
C) readFileSync cannot read UTF-8 files
D) readFileSync requires a callback and readFile does not
Q2. Given the following code, what is printed to the console if example.txt does not exist?
const fs = require('fs');
try {
const data = fs.readFileSync('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
A) An empty string
B) undefined
C) An Error object printed via console.error (likely ENOENT: no such file or directory)
D) The program crashes with no output
Q3. What is the difference between fs.writeFile and fs.appendFile?
Q4. You are building a feature that processes a 2 GB CSV export. Which fs approach should you use and why?
A) fs.readFileSync — synchronous reads are more reliable for large files
B) fs.readFile — asynchronous callback receives the whole buffer at once
C) fs.createReadStream — streams the file in chunks, keeping memory usage low
D) fs.watch — it monitors the file size as data arrives
A1. B — readFile is non-blocking. The event loop is free to serve other requests while the OS retrieves the file from disk; readFileSync stalls the entire process until the read completes.
A2. C — readFileSync throws a synchronous error when the file is missing. The catch block receives it and console.error prints the Error object, which includes the ENOENT code and the missing path.
A3. writeFile replaces the entire file contents (or creates the file if it does not exist). appendFile adds new data to the end of an existing file (or creates the file if it does not exist). Use writeFile when you want a clean overwrite; use appendFile for logs or incremental updates.
A4. C — fs.createReadStream processes the file in small chunks, so only a fraction of the 2 GB is ever in memory at once. Loading the full file with readFile or readFileSync would likely exhaust available RAM.
🪞 Recap
require('fs')gives you the entire built-in file-system API with no extra installation.- Every major operation (
readFile,writeFile,appendFile,unlink,mkdir,rmdir) has both an async (callback) and a sync (*Sync) variant — prefer async in servers. appendFileadds to the end of a file;writeFileoverwrites it entirely.fs.watchfires a callback whenever a file or directory changes, with aneventTypeof'rename'or'change'.createReadStreamandcreateWriteStreamprocess files in chunks, making them the right tool for large files where loading everything into memory would be wasteful.
📚 Further Reading
- Node.js
fsmodule docs — the source of truth for everyfsmethod, flag, and option - Node.js Streams documentation — deep dive into readable, writable, and transform streams
- Understanding Node.js Event Loop — why non-blocking I/O matters and how the event loop processes async callbacks
- ⬅️ Previous: JSON Data
- ➡️ Next: Reading POST Data