Reading and writing files with fs

Node.js · Essentials · 12 min

Config files, uploads, logs, generated output — sooner or later every Node program touches the file system.

The node:fs module offers three styles of the same operations: callbacks (fs.readFile(path, cb)), synchronous (fs.readFileSync) and promises. In application code use the promise API from `node:fs/promises` with await.

The everyday functions:

  • readFile(path, "utf8") — whole file as a string (omit the encoding to get a Buffer of bytes).
  • writeFile(path, data) — create or replace; appendFile adds to the end.
  • readdir(dir, { withFileTypes: true }) — entries with isFile() / isDirectory(); add recursive: true to walk subfolders.
  • mkdir(dir, { recursive: true }) — creates parents and doesn't fail if it already exists.
  • stat(path) — size, timestamps, type.
  • rm(path, { recursive: true, force: true }) — delete files or trees.
  • rename, copyFile, cp (recursive copy), access.

Failures reject with an error whose code tells you what happened: ENOENT (no such file), EACCES (permission), EEXIST, EISDIR. Check err.code rather than the message text.

Don't call access() or stat() to check whether a file exists before opening it — another process can change things in between. Just attempt the operation and handle ENOENT.

Synchronous functions block the event loop for the whole operation. They are fine in a CLI script or at startup (loading config once) but never inside a request handler.

readFile loads everything into memory; for large files or files you process line by line, use streams (later lesson). Relative paths resolve against the process's current working directory, not the file's location — build absolute paths with import.meta.dirname (next lesson).

Example

import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";

const dir = join(import.meta.dirname, "out");
await mkdir(dir, { recursive: true });                 // ok if it exists
await writeFile(join(dir, "notes.txt"), "first line\n");

const text = await readFile(join(dir, "notes.txt"), "utf8");
console.log(text.trim());                              // first line

for (const entry of await readdir(dir, { withFileTypes: true })) {
  console.log(entry.name, entry.isFile() ? "file" : "dir");
}

async function readConfig(path) {
  try {
    return JSON.parse(await readFile(path, "utf8"));
  } catch (err) {
    if (err.code === "ENOENT") return {};              // missing file -> defaults
    throw err;                                         // anything else is a real problem
  }
}
console.log(await readConfig("nope.json"));            // {}

Remember

  • Use node:fs/promises with await in application code
  • Handle errors by err.code (ENOENT, EACCES...), don't pre-check existence
  • Sync APIs block the event loop — only for scripts and startup

Watch out for

  • Using relative paths and being surprised when the script is run from another directory.
  • Reading huge files with readFile instead of streaming them.

Check yourself

  1. You want to load settings.json but use defaults if it doesn't exist. What's the robust approach?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/fs.html#promises-api