⚑IntermediateNode.js Core APIs

File System (fs)

Read, write, and manage files and directories with Node's built-in fs module

Overview

The `fs` module is Node.js's interface to the file system. It covers reading files, writing files, working with directories, checking file metadata, watching for changes, and more. It is one of the most heavily used built-in modules in the ecosystem β€” powering build tools, loggers, configuration loaders, CLIs, and virtually every server-side application that touches the disk. The fs module exposes three API styles. The original callback API (e.g., `fs.readFile(path, callback)`) follows the Node.js error-first convention where the first argument to the callback is an error or null. The synchronous API (e.g., `fs.readFileSync(path)`) blocks the event loop until the operation completes β€” appropriate for startup scripts, but dangerous in server request handlers. The modern Promises API (`fs/promises`) returns Promises that work cleanly with async/await and is the recommended approach for new code. When working with large files, the callback and promise APIs load the entire file into memory as a Buffer or string. For files larger than a few hundred megabytes, you should use streams instead (covered in the next topic). For everyday config files, JSON data, and logs, the standard readFile/writeFile are perfectly appropriate. Directory operations like `fs.mkdir`, `fs.readdir`, and `fs.rm` follow the same three-style pattern. The `{ recursive: true }` option on mkdir and rm is essential β€” it creates or removes nested directories without throwing errors if they already exist or are not empty, mirroring the behavior of `mkdir -p` and `rm -rf` in Unix shells.

Key Points

  • β€’Prefer fs/promises with async/await for all new code β€” it is cleaner than callbacks and non-blocking unlike sync methods
  • β€’fs.readFile returns a Buffer by default; pass { encoding: "utf8" } or "utf8" as the second argument to get a string
  • β€’fs.writeFile creates or truncates; fs.appendFile adds to existing content
  • β€’fs.mkdir with { recursive: true } is the equivalent of mkdir -p β€” it creates parent directories and ignores "already exists" errors
  • β€’fs.stat and fs.access check whether a file exists and get metadata (size, mtime, isDirectory)
  • β€’Always handle errors β€” files may not exist, permissions may be denied, or the disk may be full

Examples

Reading a file with fs/promises (recommended async/await style)

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

async function readConfig() {
  try {
    const content = await fs.readFile('config.json', 'utf8');
    const config  = JSON.parse(content);
    console.log('Config loaded:', config);
    return config;
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('Config file not found, using defaults');
      return {};
    }
    throw err; // re-throw unexpected errors
  }
}

readConfig();

err.code === "ENOENT" means "file not found" β€” always handle this case specifically when a missing file is a recoverable condition.

Writing, appending, and reading back a file

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

async function run() {
  // Write (creates or overwrites)
  await fs.writeFile('output.txt', 'Line 1\n', 'utf8');

  // Append to existing content
  await fs.appendFile('output.txt', 'Line 2\n', 'utf8');

  // Read back
  const content = await fs.readFile('output.txt', 'utf8');
  console.log(content);
  // Line 1
  // Line 2
}

run();

Working with directories

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

async function setup() {
  // Create nested directory (like mkdir -p)
  await fs.mkdir('dist/assets/images', { recursive: true });

  // List directory contents
  const entries = await fs.readdir('src', { withFileTypes: true });
  for (const entry of entries) {
    const type = entry.isDirectory() ? 'dir ' : 'file';
    console.log(`${type}: ${entry.name}`);
  }
}

setup();

Checking file metadata with fs.stat

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

async function fileInfo(filePath) {
  try {
    const stats = await fs.stat(filePath);
    console.log('Size (bytes):', stats.size);
    console.log('Is directory:', stats.isDirectory());
    console.log('Last modified:', stats.mtime);
    console.log('Is file:', stats.isFile());
  } catch (err) {
    if (err.code === 'ENOENT') console.log('File not found');
    else throw err;
  }
}

fileInfo('package.json');

Common Mistakes

  • Using fs.readFileSync or fs.writeFileSync inside HTTP request handlers β€” these block the event loop and kill server throughput
  • Forgetting { encoding: "utf8" } on readFile and getting a Buffer instead of a string
  • Not handling ENOENT specifically β€” crashing when a missing file is a normal recoverable condition
  • Using fs.exists() which is deprecated β€” use fs.access() or wrap fs.stat() in a try/catch instead

Note

For reading JSON config files that are needed at startup, using the synchronous API is fine β€” the process is not yet handling requests so blocking is acceptable. But once your HTTP server is listening, always use the async API for any file I/O triggered by requests.

Exercises

Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.

Loading editor…

Completed: 0 / 6 exercises

Knowledge Check

Question 1 of 4

What does fs.readFile return if you do not pass an encoding option?