File System

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

This tutorial assumes familiarity with the event loop and Node's asynchronous execution model.

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 API

3. 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.

StyleExampleBlocks Event Loop?
Synchronousfs.readFileSync()Yes
Callbackfs.readFile()No
Promise-basedfsPromises.readFile()No

Warning

Avoid Sync methods in request handlers or other hot paths — they freeze the entire process for every concurrent request.

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

Omitting the encoding argument (e.g. 'utf-8') returns a raw Buffer instead of a string.

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

writeFile overwrites the entire file by default. Use append mode or fs.appendFile to add content instead.

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

fs.rename() can fail across different filesystems/drives (e.g. moving between separate mounted volumes). In that case, fall back to copy then delete.

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

The recursive: true option creates any missing parent directories automatically.

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

{ recursive: true, force: true } deletes a folder and everything inside it without error if it doesn't exist — double-check the path before running this in production code.

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-x

Information

On Windows, most permission bits are ignored — only the read-only flag has a meaningful effect.

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);
PropertyDescription
sizeFile size in bytes
isFile() / isDirectory()Type checks
mtimeLast modified timestamp
birthtimeCreation 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

fs.watch() behavior is platform-dependent and can fire duplicate or missed events — for production use cases, consider a battle-tested library like chokidar.

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

Always use .pipe() or the stream/promises pipeline() helper to handle backpressure automatically.

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

Use os.tmpdir() to get the OS-appropriate temporary directory rather than hardcoding /tmp.

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;
  }
}
CodeMeaning
ENOENTNo such file or directory
EACCESPermission denied
EEXISTFile already exists
EISDIRExpected 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

See the official Node.js fs module documentation for the complete API 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.

25. Frequently Asked Questions ❓

Question

Should I use fs callbacks, fs/promises, or fs-Sync?

Answer

Use fs/promises with async/await for almost everything. Reserve Sync methods for startup-time scripts or CLI tools where blocking briefly is acceptable.

Question

How do I check if a file exists?

Answer

Use fs.access() or attempt the operation directly and catch an ENOENT error — avoid the deprecated fs.exists().

Question

Why did my stream stop mid-way with no error?

Answer

This is often a sign of unhandled backpressure or a missing 'error' listener — use pipeline() from stream/promises to manage this correctly.

26. Summary 📝

Summary

Node's fs module provides synchronous, callback, and Promise-based APIs for reading, writing, and managing files and directories. For anything performance-sensitive, prefer fs/promises and streams over blocking or fully-buffered operations. Understanding Buffers, error codes, and permissions rounds out the toolkit needed to build reliable file-handling logic in real applications.