Node.js File System

Node's built-in fs module gives you full control over files—reading, writing, appending, and deleting—through synchronous, callback-based, and promise-based APIs.

The fs Module

Node.js ships with a core module called fs (short for file system) that lets you interact with files and directories without installing anything from npm. You bring it into a script with require('node:fs') or, in ESM files, import fs from 'node:fs'. Everything you'd expect from a file system API lives here: reading a file's contents, creating a new file, appending a log line, renaming a directory, or deleting a temporary file.

Synchronous vs Asynchronous Operations

Almost every fs function comes in three forms. A synchronous version, whose name ends in Sync, runs to completion and blocks the entire process until the disk operation finishes. A callback-based asynchronous version takes a function as its last argument and runs it once the operation completes, without blocking anything else. A promise-based version, available from fs/promises, returns a Promise so it pairs naturally with async/await. Picking the right one depends on where the code runs: a one-off CLI script that reads a config file at startup can safely use the Sync methods, but a running server should never use them, since blocking the event loop means every other request has to wait.

  • readFileSync / writeFileSync / appendFileSync — simplest to reason about, but freeze the event loop until they finish
  • fs.readFile / fs.writeFile / fs.appendFile — classic Node error-first callback style, fully non-blocking
  • fs.promises.readFile (or import from 'node:fs/promises') — async/await friendly, fully non-blocking
  • fs.appendFile — adds content to the end of a file without overwriting what's already there
  • fs.unlink — deletes a file from disk; fs.rm can delete files or, with { recursive: true }, whole directories

Synchronous read, write, and append

const fs = require('node:fs');

// Blocks execution until the file is fully saved
fs.writeFileSync('greeting.txt', 'Hello, Node.js!\n');

// Blocks execution until the file is fully loaded
const contents = fs.readFileSync('greeting.txt', 'utf8');
console.log(contents); // Hello, Node.js!

// Append a second line without overwriting the first
fs.appendFileSync('greeting.txt', 'Second line added.\n');
console.log(fs.readFileSync('greeting.txt', 'utf8'));

Callback-Based I/O and the Promises API

The callback style was Node's original approach to non-blocking file access, and it follows a strict convention: every callback receives an error object first, so if the operation succeeded that first argument is simply null. This convention is called an 'error-first callback', and you'll see it throughout older Node code and libraries. Since Node 10, the same operations are also available through fs.promises (or the standalone node:fs/promises module), where each method returns a Promise instead—letting you write flat, readable code with async/await and catch failures with an ordinary try/catch block.

Callback-based read, append, and delete

const fs = require('node:fs');

fs.writeFile('scratch.txt', 'draft entry\n', (err) => {
  if (err) throw err;

  fs.appendFile('scratch.txt', 'second entry\n', (err) => {
    if (err) throw err;

    fs.readFile('scratch.txt', 'utf8', (err, data) => {
      if (err) throw err;
      console.log(data);

      fs.unlink('scratch.txt', (err) => {
        if (err) throw err;
        console.log('scratch.txt deleted');
      });
    });
  });
});
StyleModuleBlocks event loop?Error handling
Synchronousfs (methods ending in Sync)Yestry/catch around the call
CallbackfsNoError-first callback argument
Promisefs/promisesNotry/catch with async/await, or .catch()

Promise-based file operations with async/await

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

async function runLog() {
  try {
    await fs.writeFile('app.log', 'server started\n');
    await fs.appendFile('app.log', 'listening on port 3000\n');

    const log = await fs.readFile('app.log', 'utf8');
    console.log(log);

    await fs.unlink('app.log');
    console.log('log file removed');
  } catch (err) {
    console.error('File operation failed:', err.message);
  }
}

runLog();
Note: Always pass an encoding like 'utf8' when you want a string back. Without it, fs.readFile and fs.readFileSync return a raw Buffer of bytes, which is correct for binary files (images, zip archives) but not what you want when printing text.

Exercise: Node.js File System

By default, is fs.readFile() synchronous or asynchronous?