File System Operations

Difficulty: Beginner

Question

How do you perform file system operations in Node.js? Show the difference between callback, sync, and promise-based approaches.

Answer

The fs module provides three APIs for file operations: callback-based (fs.readFile), synchronous (fs.readFileSync), and promise-based (fs/promises). The promise-based API is the modern recommended approach as it works cleanly with async/await.

Common operations include reading files, writing files, appending data, creating directories, listing directory contents, checking if files exist, and watching for file changes. Each has callback, sync, and promise variants.

The synchronous API blocks the event loop and should only be used during startup or in CLI scripts. The callback API is the original Node.js pattern. The promise API (fs/promises) introduced in Node 10+ is now the preferred approach for application code.

Always handle errors when working with the file system. Files may not exist, permissions may be wrong, or the disk may be full. Use try/catch with async/await or .catch() with promises.

Code examples

Three File Reading Approaches

const fs = require('fs');
const fsPromises = require('fs/promises');

// 1. Callback (old pattern)
fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error:', err.message);
    return;
  }
  console.log('Callback:', data);
});

// 2. Synchronous (blocks event loop!)
try {
  const data = fs.readFileSync('data.txt', 'utf8');
  console.log('Sync:', data);
} catch (err) {
  console.error('Error:', err.message);
}

// 3. Promise-based (recommended)
async function readData() {
  try {
    const data = await fsPromises.readFile('data.txt', 'utf8');
    console.log('Async:', data);
  } catch (err) {
    console.error('Error:', err.message);
  }
}
readData();

Promise-based fs is cleanest with async/await. Sync blocks the loop. Callbacks lead to nesting. Always specify encoding for text files.

Writing Files and Directory Operations

const fs = require('fs/promises');
const path = require('path');

async function fileOperations() {
  const dir = path.join(__dirname, 'output');

  // Create directory (recursive handles nested paths)
  await fs.mkdir(dir, { recursive: true });

  // Write file (creates or overwrites)
  await fs.writeFile(
    path.join(dir, 'report.json'),
    JSON.stringify({ status: 'ok', count: 42 }, null, 2)
  );

  // Append to file
  await fs.appendFile(
    path.join(dir, 'log.txt'),
    `[${new Date().toISOString()}] Server started\n`
  );

  // List directory contents
  const files = await fs.readdir(dir);
  console.log('Files:', files);

  // Get file info
  const stats = await fs.stat(path.join(dir, 'report.json'));
  console.log('Size:', stats.size, 'bytes');
  console.log('Is file:', stats.isFile());
}
fileOperations();

mkdir with recursive: true is like mkdir -p. writeFile overwrites, appendFile adds to end. stat gives file metadata.

File Existence Check and Copy

const fs = require('fs/promises');
const { constants } = require('fs');

async function safeFileCopy(src, dest) {
  // Check if source exists and is readable
  try {
    await fs.access(src, constants.R_OK);
  } catch {
    throw new Error(`Source file not readable: ${src}`);
  }

  // Check if destination already exists
  try {
    await fs.access(dest);
    console.log('Destination exists, will overwrite');
  } catch {
    // File doesn't exist - that's fine
  }

  // Copy the file
  await fs.copyFile(src, dest);
  console.log(`Copied ${src} -> ${dest}`);

  // Rename/move a file
  await fs.rename(dest, dest + '.bak');

  // Delete a file
  await fs.unlink(dest + '.bak');
}
safeFileCopy('input.txt', 'output.txt');

Use fs.access() to check permissions, not fs.exists() (deprecated). copyFile, rename, and unlink are the core file manipulation operations.

Key points

Concepts covered

fs module, Promises API, readFile, writeFile, directory operations