Node.js File System Module: Read, Write & Manipulate Files
The Node.js File System (fs) module provides a powerful API for interacting with the file system. If you’re new to Node.js’s architecture, it helps to first understand how Node.js handles threading and async I/O — the fs module’s async methods work because of the libuv thread pool described there. Whether you need to read, write, delete, watch, or stream files, the fs module has you covered. This guide covers the most important methods with practical examples — from basic reads to streaming large files with createReadStream.
To use the module, require it at the top of your file:
const fs = require('fs');
Or use the promise-based API (recommended for modern Node.js):
const fs = require('fs/promises');
Quick Reference
| Task | Async (callback) | Promise-based | Sync |
|---|---|---|---|
| Read file | fs.readFile | fs.promises.readFile | fs.readFileSync |
| Write file | fs.writeFile | fs.promises.writeFile | fs.writeFileSync |
| Append to file | fs.appendFile | fs.promises.appendFile | fs.appendFileSync |
| Delete file | fs.unlink | fs.promises.unlink | fs.unlinkSync |
| Rename file | fs.rename | fs.promises.rename | fs.renameSync |
| Create directory | fs.mkdir | fs.promises.mkdir | fs.mkdirSync |
| File metadata | fs.stat | fs.promises.stat | fs.statSync |
| Watch for changes | fs.watch | — | — |
| Stream large files | fs.createReadStream | — | — |
Reading Files
Asynchronous (Callback)
fs.readFile reads a file’s contents without blocking the event loop:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File contents:', data);
});
Promise-based with async/await (Recommended)
const fs = require('fs/promises');
async function readFile() {
try {
const data = await fs.readFile('example.txt', 'utf8');
console.log('File contents:', data);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('File not found');
} else {
throw err;
}
}
}
readFile();
Checking err.code lets you handle specific errors (missing file, permission denied) without swallowing all errors.
Synchronous
fs.readFileSync blocks the event loop until the file is read. Only use this in scripts or startup code where blocking is acceptable — never in a web server request handler:
try {
const data = fs.readFileSync('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('Error:', err);
}
Writing Files
Async/Await
fs.writeFile creates the file if it doesn’t exist and overwrites it if it does:
const fs = require('fs/promises');
async function writeFile() {
try {
await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
console.log('File written successfully');
} catch (err) {
console.error('Error writing file:', err);
}
}
Appending to a File
To add content without overwriting existing data, use appendFile:
await fs.appendFile('log.txt', `[${new Date().toISOString()}] Event occurred\n`, 'utf8');
This is useful for log files where you want to accumulate entries over time.
Manipulating Files and Directories
Renaming Files
const fs = require('fs/promises');
await fs.rename('old-name.txt', 'new-name.txt');
fs.rename also works for moving files between directories:
await fs.rename('uploads/temp.txt', 'processed/final.txt');
Deleting Files
await fs.unlink('example.txt');
To delete a directory and all its contents recursively:
await fs.rm('my-folder', { recursive: true, force: true });
Creating Directories
// Create a single directory
await fs.mkdir('new-folder');
// Create nested directories (like mkdir -p)
await fs.mkdir('a/b/c', { recursive: true });
Checking File Metadata with fs.stat
fs.stat returns metadata about a file or directory — size, type, timestamps, permissions:
const fs = require('fs/promises');
async function getFileInfo(path) {
try {
const stats = await fs.stat(path);
console.log('Is file:', stats.isFile());
console.log('Is directory:', stats.isDirectory());
console.log('Size (bytes):', stats.size);
console.log('Created:', stats.birthtime);
console.log('Last modified:', stats.mtime);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('Path does not exist');
}
}
}
getFileInfo('example.txt');
Checking if a File Exists
fs.existsSync is the simplest check, but the async pattern with fs.stat is more robust:
async function fileExists(path) {
try {
await fs.stat(path);
return true;
} catch {
return false;
}
}
Watching Files for Changes with fs.watch
fs.watch fires a callback whenever a file or directory is modified. Useful for build tools, hot-reloading, and file sync systems:
const fs = require('fs');
const watcher = fs.watch('config.json', (eventType, filename) => {
console.log(`Event: ${eventType}`);
if (filename) {
console.log(`File changed: ${filename}`);
}
});
// Stop watching after 30 seconds
setTimeout(() => watcher.close(), 30000);
eventType is either 'rename' (file created, deleted, or renamed) or 'change' (file content modified).
Note:
fs.watchcan fire multiple times for a single save (depending on the editor). For production use, debounce the callback or use a library like chokidar which normalizes cross-platform behavior.
Streaming Large Files with createReadStream
For large files, loading the entire contents into memory with readFile is inefficient and can crash your process. Use streams instead — they process data in chunks:
Reading a Large File
const fs = require('fs');
const readStream = fs.createReadStream('large-file.csv', {
encoding: 'utf8',
highWaterMark: 64 * 1024, // 64 KB chunks
});
readStream.on('data', (chunk) => {
console.log('Received chunk:', chunk.length, 'bytes');
});
readStream.on('end', () => {
console.log('Finished reading file');
});
readStream.on('error', (err) => {
console.error('Stream error:', err);
});
Copying a File with Streams (pipe)
const fs = require('fs');
const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('destination.txt');
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('File copied successfully');
});
pipe automatically handles backpressure — it pauses reading when the write buffer is full, preventing memory overflow.
When to Use Streams vs readFile
| Scenario | Use |
|---|---|
| Config files, small JSON | fs.readFile / fs.promises.readFile |
| Log files, CSVs > 10 MB | fs.createReadStream |
| Serving files over HTTP | fs.createReadStream().pipe(res) |
| Processing files line-by-line | createReadStream + readline interface |
FAQs
Q: Can I read and write binary files with the fs module?
A: Yes. Omit the encoding option to get a raw Buffer object instead of a string. For example: fs.readFile('image.png', (err, buffer) => { ... }).
Q: How do I check if a file exists before reading it?
A: Use fs.stat in a try/catch and check for err.code === 'ENOENT'. Avoid fs.existsSync followed by fs.readFile — the file could be deleted between the two calls (TOCTOU race condition).
Q: What is the difference between fs.unlink and fs.rm?
A: fs.unlink removes files only. fs.rm is more versatile — it can remove both files and directories, and supports { recursive: true } for non-empty directories.
Q: Why does my fs.watch callback fire twice on every save?
A: Many editors (VS Code, Vim) write a temp file and then rename it, which triggers two events. Debounce the callback with a short delay (e.g., 100ms) to deduplicate.
Q: Is fs/promises available in older Node.js versions?
A: fs.promises has been available since Node.js 10. The cleaner require('fs/promises') shorthand was added in Node.js 14. For Node.js 12, use const { promises: fs } = require('fs') instead.
Q: How do I process a large file line by line?
A: Combine createReadStream with the readline module:
const fs = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream('large.csv'),
});
rl.on('line', (line) => {
console.log('Line:', line);
});
Key Takeaways
- Use
fs/promiseswith async/await for modern, clean Node.js code — it’s the recommended approach. - Use sync methods (
readFileSync,writeFileSync) only in scripts or startup code, never in request handlers. - Use
fs.statto get file metadata and check existence without reading the full file. - Use
fs.watchto react to file changes — debounce it or usechokidarfor production. - Use streams (
createReadStream/createWriteStream) for files larger than a few MB to keep memory usage flat. - Always check
err.codein catch blocks to handle specific errors (file not found, permission denied) gracefully. - To understand how
require('fs')loads the module under the hood, see how require() works in CommonJS.
Related Articles
app.use vs app.all in Express.js: Key Differences Explained
Learn the difference between app.use and app.all in Express.js — middleware vs route handling, partial vs exact path matching, error-handling middleware, router.use, and real auth middleware examples.
Node.jsIs Node.js Single-Threaded or Multi-Threaded? A Complete Guide
Understand how Node.js handles concurrency — single-threaded JavaScript execution, the event loop, libuv thread pool, worker threads for CPU tasks, the cluster module, and when to use each approach.
Node.jsHow require() Works in Node.js: CommonJS Module Loading Explained
Deep dive into how Node.js require() resolves module paths, checks the module cache, wraps code in closures, executes modules, returns exports — plus ESM vs CommonJS comparison, dynamic import(), and circular dependency behavior.
Written by
Aditya RawasFull-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.