File System
The file system module is one of the most frequently used parts of Node.js. Modern applications should prefer the promise-based fs/promises API over callback-based fs or synchronous fs.*Sync methods. Promises compose better with async/await and do not block the event loop.
File system operations in Node.js run on the libuv thread pool. While this prevents direct event loop blocking, excessive file I/O can still saturate the thread pool and delay other operations like DNS lookups and cryptographic hashing.
This guide covers reading, writing, streaming, watching, and managing files and directories safely in production.
Use readFile for small files that fit comfortably in memory. For large files, use streams to keep memory usage constant. Always handle errors and validate paths.
| 1 | const fs = require('fs/promises'); |
| 2 | |
| 3 | async function readConfig(path) { |
| 4 | try { |
| 5 | const data = await fs.readFile(path, 'utf8'); |
| 6 | return JSON.parse(data); |
| 7 | } catch (err) { |
| 8 | if (err.code === 'ENOENT') { |
| 9 | console.error('Config file not found:', path); |
| 10 | return null; |
| 11 | } |
| 12 | throw err; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | async function writeConfig(path, config) { |
| 17 | await fs.writeFile(path, JSON.stringify(config, null, 2), { |
| 18 | encoding: 'utf8', |
| 19 | mode: 0o600 // readable/writable by owner only |
| 20 | }); |
| 21 | } |
warning
Streams are the right tool for large files. The stream/promises pipeline helper handles errors, backpressure, and resource cleanup automatically.
| 1 | const fs = require('fs'); |
| 2 | const { pipeline } = require('stream/promises'); |
| 3 | const zlib = require('zlib'); |
| 4 | |
| 5 | async function compressLog(inputPath, outputPath) { |
| 6 | await pipeline( |
| 7 | fs.createReadStream(inputPath), |
| 8 | zlib.createGzip(), |
| 9 | fs.createWriteStream(outputPath) |
| 10 | ); |
| 11 | } |
| 12 | |
| 13 | compressLog('./app.log', './app.log.gz') |
| 14 | .then(() => console.log('Compressed')) |
| 15 | .catch((err) => { |
| 16 | console.error('Compression failed:', err); |
| 17 | process.exit(1); |
| 18 | }); |
best practice
Node.js 20+ provides recursive directory creation and removal natively. The fs.opendir API lets you iterate over directory entries without loading the entire listing into memory.
| 1 | const fs = require('fs/promises'); |
| 2 | const path = require('path'); |
| 3 | |
| 4 | async function ensureDir(dirPath) { |
| 5 | await fs.mkdir(dirPath, { recursive: true }); |
| 6 | } |
| 7 | |
| 8 | async function* walk(dir) { |
| 9 | const entries = await fs.opendir(dir); |
| 10 | for await (const entry of entries) { |
| 11 | const fullPath = path.join(dir, entry.name); |
| 12 | if (entry.isDirectory()) { |
| 13 | yield* walk(fullPath); |
| 14 | } else { |
| 15 | yield fullPath; |
| 16 | } |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | async function countFiles(dir) { |
| 21 | let count = 0; |
| 22 | for await (const file of walk(dir)) { |
| 23 | console.log(file); |
| 24 | count++; |
| 25 | } |
| 26 | return count; |
| 27 | } |
info
fs.watch uses platform-specific notification mechanisms. It can emit multiple events for a single change and may not work reliably across network filesystems. For robust watching, consider fs.watchFile polling or dedicated libraries like Chokidar.
| 1 | const fs = require('fs/promises'); |
| 2 | |
| 3 | async function watchFile(filePath) { |
| 4 | const watcher = fs.watch(filePath); |
| 5 | |
| 6 | try { |
| 7 | for await (const event of watcher) { |
| 8 | console.log('Event:', event.eventType, 'File:', event.filename); |
| 9 | } |
| 10 | } catch (err) { |
| 11 | console.error('Watch error:', err); |
| 12 | } finally { |
| 13 | watcher.close(); |
| 14 | } |
| 15 | } |
note
File system security involves more than reading and writing. Create files with restrictive permissions, validate paths to prevent directory traversal, and avoid executing files from untrusted sources.
| 1 | const path = require('path'); |
| 2 | |
| 3 | function safeJoin(base, target) { |
| 4 | const resolved = path.resolve(base, target); |
| 5 | if (!resolved.startsWith(path.resolve(base))) { |
| 6 | throw new Error('Directory traversal detected'); |
| 7 | } |
| 8 | return resolved; |
| 9 | } |
| 10 | |
| 11 | // safeJoin('/data', '../etc/passwd') throws |
| 12 | // safeJoin('/data', 'user/profile.json') returns /data/user/profile.json |
warning
| Code | Meaning | Typical Cause |
|---|---|---|
| ENOENT | No such file or directory | Missing file, typo in path |
| EACCES | Permission denied | Wrong owner or restrictive mode |
| EISDIR | Is a directory | readFile on a directory path |
| EMFILE | Too many open files | File descriptor leak |
| EEXIST | File already exists | Exclusive create conflict |