1. Introduction đ
Almost every backend application needs to read, write, or inspect files at some point â configs, logs, uploads, cached data. Node's built-in fs module gives you full control over the file system, from simple reads to low-level binary and stream manipulation.
Information
2. File System Module đī¸
The fs module (import via node:fs) is Node's core API for interacting with the file system. It exposes three parallel styles for nearly every operation: callback-based, synchronous, and Promise-based.
fs-imports.js
const fs = require('node:fs'); // callback + sync API
const fsPromises = require('node:fs/promises'); // Promise-based API3. Synchronous vs Asynchronous APIs âŗ
Every core fs method comes in a synchronous form (suffixed Sync) and an asynchronous form. Synchronous methods block the event loop until they complete; asynchronous methods delegate to libuv's thread pool and return control immediately.
| Style | Example | Blocks Event Loop? |
|---|---|---|
| Synchronous | fs.readFileSync() | Yes |
| Callback | fs.readFile() | No |
| Promise-based | fsPromises.readFile() | No |
Warning
4. Reading Files đ
read-file.js
const fs = require('node:fs/promises');
async function readConfig() {
const data = await fs.readFile('./config.json', 'utf-8');
console.log(JSON.parse(data));
}Tip
5. Writing Files âī¸
write-file.js
const fs = require('node:fs/promises');
async function saveLog(message) {
await fs.writeFile('./output.log', message, 'utf-8');
}Caution
6. Appending Files â
append-file.js
const fs = require('node:fs/promises');
async function logEvent(event) {
await fs.appendFile('./events.log', `${event}\n`);
}7. Copying Files đ
copy-file.js
const fs = require('node:fs/promises');
await fs.copyFile('./source.txt', './backup.txt');8. Renaming Files đˇī¸
rename-file.js
const fs = require('node:fs/promises');
await fs.rename('./old-name.txt', './new-name.txt');9. Moving Files đ
Node has no dedicated move function â fs.rename() handles both renaming and moving, since a move is just a rename to a different path.
move-file.js
const fs = require('node:fs/promises');
await fs.rename('./uploads/temp.png', './images/final.png');Caution
10. Deleting Files đī¸
delete-file.js
const fs = require('node:fs/promises');
await fs.unlink('./temp.txt');11. Directory Operations đ
11.1 Creating Directories
create-dir.js
const fs = require('node:fs/promises');
await fs.mkdir('./uploads/images', { recursive: true });Tip
11.2 Reading Directories
read-dir.js
const fs = require('node:fs/promises');
const entries = await fs.readdir('./uploads', { withFileTypes: true });
for (const entry of entries) {
console.log(entry.name, entry.isDirectory() ? '(dir)' : '(file)');
}11.3 Removing Directories
remove-dir.js
const fs = require('node:fs/promises');
await fs.rm('./uploads/old-folder', { recursive: true, force: true });Danger
12. File Permissions đ
File permissions are represented as an octal mode (e.g. 0o644), following the Unix convention of owner / group / others read-write-execute bits.
chmod-example.js
const fs = require('node:fs/promises');
await fs.chmod('./script.sh', 0o755); // rwxr-xr-xInformation
13. File Statistics đ
stat-example.js
const fs = require('node:fs/promises');
const stats = await fs.stat('./report.pdf');
console.log(stats.size, stats.isFile(), stats.mtime);| Property | Description |
|---|---|
| size | File size in bytes |
| isFile() / isDirectory() | Type checks |
| mtime | Last modified timestamp |
| birthtime | Creation timestamp |
14. Watching Files đ
watch-file.js
const fs = require('node:fs');
fs.watch('./config.json', (eventType, filename) => {
console.log(`Detected ${eventType} on ${filename}`);
});Warning
15. Working with Buffers đ§ĩ
A Buffer is Node's structure for handling raw binary data directly, outside V8's string encoding. Reading a file without specifying an encoding returns a Buffer.
buffer-example.js
const fs = require('node:fs/promises');
const data = await fs.readFile('./image.png'); // Buffer
console.log(data.length, data[0]); // byte length, first byte
const buf = Buffer.from('hello', 'utf-8');
console.log(buf.toString('hex'));16. File Streams đ
Streams read or write data in chunks instead of loading an entire file into memory â essential for large files or network transfers.
stream-example.js
const fs = require('node:fs');
const readStream = fs.createReadStream('./large-video.mp4');
const writeStream = fs.createWriteStream('./copy.mp4');
readStream.pipe(writeStream);
readStream.on('end', () => console.log('Copy complete'));
readStream.on('error', (err) => console.error('Read error:', err));Best Practice
17. Large File Handling đĻ
For files too large to fit comfortably in memory, always prefer streaming over readFile/writeFile. Process the file incrementally, and consider reading in fixed-size chunks with explicit file descriptors for random access.
pipeline-example.js
const { pipeline } = require('node:stream/promises');
const fs = require('node:fs');
const zlib = require('node:zlib');
await pipeline(
fs.createReadStream('./access.log'),
zlib.createGzip(),
fs.createWriteStream('./access.log.gz')
);18. Temporary Files đī¸
tmp-file.js
const os = require('node:os');
const path = require('node:path');
const fs = require('node:fs/promises');
const tmpPath = path.join(os.tmpdir(), `upload-${Date.now()}.tmp`);
await fs.writeFile(tmpPath, 'temporary data');Tip
19. JSON Files đ§ž
json-file.js
const fs = require('node:fs/promises');
async function readJSON(path) {
const raw = await fs.readFile(path, 'utf-8');
return JSON.parse(raw);
}
async function writeJSON(path, data) {
await fs.writeFile(path, JSON.stringify(data, null, 2));
}20. Binary Files đž
Binary files (images, executables, archives) should be read and written without a text encoding, keeping data as raw Buffer objects to avoid corruption.
binary-copy.js
const fs = require('node:fs/promises');
const imageBuffer = await fs.readFile('./photo.jpg');
await fs.writeFile('./photo-copy.jpg', imageBuffer);21. Error Handling â ī¸
fs errors carry a code property (e.g. ENOENT, EACCES, EEXIST) that should be checked to handle specific failure cases gracefully.
error-handling.js
const fs = require('node:fs/promises');
try {
await fs.readFile('./missing.txt', 'utf-8');
} catch (err) {
if (err.code === 'ENOENT') {
console.error('File does not exist');
} else {
throw err;
}
}| Code | Meaning |
|---|---|
| ENOENT | No such file or directory |
| EACCES | Permission denied |
| EEXIST | File already exists |
| EISDIR | Expected a file but found a directory |
22. Performance Optimization âĄ
- Prefer streams over full-file reads for anything above a few megabytes.
- Batch small writes instead of calling appendFile in a tight loop.
- Cache fs.stat() results when polling the same file repeatedly.
- Increase UV_THREADPOOL_SIZE if I/O-heavy code is bottlenecked on the default 4-thread pool.
23. Best Practices â
- Always use the Promise-based fs/promises API in modern async code.
- Validate and sanitize any user-supplied file paths to prevent path traversal vulnerabilities.
- Use { recursive: true } intentionally, and confirm paths before destructive operations.
- Handle every specific err.code you expect, and rethrow unexpected ones.
Reference
24. Common Mistakes â ī¸
- Using fs.readFileSync inside an HTTP request handler, blocking all concurrent requests.
- Forgetting to handle the 'error' event on streams, causing unhandled exceptions.
- Not checking err.code and instead matching on the raw error message string, which can change between Node versions.
- Loading an entire large file into memory with readFile when a stream would suffice.
- Concatenating user input directly into file paths without validation.