Node.js January 7, 2025 Aditya Rawas

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

TaskAsync (callback)Promise-basedSync
Read filefs.readFilefs.promises.readFilefs.readFileSync
Write filefs.writeFilefs.promises.writeFilefs.writeFileSync
Append to filefs.appendFilefs.promises.appendFilefs.appendFileSync
Delete filefs.unlinkfs.promises.unlinkfs.unlinkSync
Rename filefs.renamefs.promises.renamefs.renameSync
Create directoryfs.mkdirfs.promises.mkdirfs.mkdirSync
File metadatafs.statfs.promises.statfs.statSync
Watch for changesfs.watch
Stream large filesfs.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);
});
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.watch can 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

ScenarioUse
Config files, small JSONfs.readFile / fs.promises.readFile
Log files, CSVs > 10 MBfs.createReadStream
Serving files over HTTPfs.createReadStream().pipe(res)
Processing files line-by-linecreateReadStream + 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/promises with 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.stat to get file metadata and check existence without reading the full file.
  • Use fs.watch to react to file changes — debounce it or use chokidar for production.
  • Use streams (createReadStream/createWriteStream) for files larger than a few MB to keep memory usage flat.
  • Always check err.code in 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.
Aditya Rawas

Written by

Aditya Rawas

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