Node.js · Modules

File System

Logs, uploads, reports, configuration — servers work with files all the time. Node’s fs/promises module does it asynchronously, so your app keeps serving users while the disk works.

Beginner 6 min Lesson 7 of 37Examples target Node.js 22 LTS.
Before you start:ModulesAsync and Await
0/37 in Node.js
On this page · 6 min · Beginner

Learning objectives#

  • Read and write text and JSON files
  • Create, list and delete folders
  • Check whether files exist and read their size
  • Handle missing files and other errors

Explanation#

Use the promise API: import { readFile, writeFile } from "node:fs/promises" and await the calls. Avoid the *Sync versions in servers — they block the event loop.

readFile(path, "utf8") returns text; writeFile replaces a file; appendFile adds to the end. For JSON, JSON.stringify before writing and JSON.parse after reading.

mkdir(dir, { recursive: true }) creates nested folders (and does not fail if they exist); readdir lists entries; stat gives size and dates; rm(path, { recursive: true, force: true }) deletes.

Errors carry a code: ENOENT (not found), EACCES (permission denied), EEXIST (already exists). Catch and check error.code.

Build paths with path.join and absolute bases (import.meta.dirname), never string concatenation — especially with user input.

Syntax#

JavaScript Illustrative
import { readFile, writeFile, mkdir } from 'node:fs/promises';
await mkdir('data', { recursive: true });
await writeFile('data/notes.txt', 'Hello');
const text = await readFile('data/notes.txt', 'utf8');

Code example: A tiny JSON “database” on disk#

JavaScriptfiles.js Illustrative
import { mkdtemp, mkdir, writeFile, readFile, appendFile, readdir, stat, rm } from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';


const root = await mkdtemp(path.join(os.tmpdir(), 'shop-'));
const dataDir = path.join(root, 'data');
await mkdir(dataDir, { recursive: true });


// Write and read JSON
const productsFile = path.join(dataDir, 'products.json');
await writeFile(productsFile, JSON.stringify([{ id: 1, name: 'Pen', price: 20 }], null, 2));
const products = JSON.parse(await readFile(productsFile, 'utf8'));
products.push({ id: 2, name: 'Notebook', price: 60 });
await writeFile(productsFile, JSON.stringify(products, null, 2));
console.log('Products:', products.map(p => p.name).join(', '));


// Append to a log
const logFile = path.join(dataDir, 'app.log');
await appendFile(logFile, 'started\n');
await appendFile(logFile, 'saved products\n');
console.log('Log lines:', (await readFile(logFile, 'utf8')).trim().split('\n').length);


// List and inspect
const entries = await readdir(dataDir);
console.log('Files:', entries.sort().join(', '));
console.log('products.json is', (await stat(productsFile)).size > 0 ? 'not empty' : 'empty');


// Handle a missing file
try {
  await readFile(path.join(dataDir, 'missing.json'), 'utf8');
} catch (err) {
  console.log('Missing file error code:', err.code);
}


await rm(root, { recursive: true, force: true });
console.log('Cleaned up');
Expected output
Products: Pen, Notebook
Log lines: 2
Files: app.log, products.json
products.json is not empty
Missing file error code: ENOENT
Cleaned up

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. mkdtemp creates a unique temporary folder so the example never touches your real files.
  2. JSON is stringified (pretty-printed with 2 spaces) before writing and parsed after reading.
  3. appendFile adds lines without rewriting the file.
  4. readdir and stat inspect the folder and file size.
  5. A missing file throws an error whose code is ENOENT.
  6. rm with recursive and force removes everything at the end.

Real-world use case#

An invoicing service writes generated PDFs into dated folders (invoices/2026/10/), keeps a daily append-only audit log, and cleans up temporary uploads older than 24 hours.

Common mistakes#

Sync methods in request handlers

readFileSync on a 50 MB file freezes the server. Use the async API or streams.

Path traversal

path.join(uploads, req.query.file) with "../../.env" escapes the folder. Validate names and check the resolved path stays inside the base folder.

Concurrent writes to one JSON file

Two requests writing at once can lose data. Use a real database for shared data.

Best practices#

  • Use fs/promises with await
  • Build paths with path.join/resolve
  • Check error.code for expected errors
  • Use a database, not JSON files, for multi-user data

Remember this#

fs/promises + await = non-blocking files.

ENOENT = file not found.

JSON files: stringify to write, parse to read.

Practice exercise#

Your task: Write a function readJson(file, fallback) that returns the parsed JSON, or the fallback value if the file does not exist (ENOENT), and rethrows other errors.

Need a hint?
try { return JSON.parse(await readFile(file, "utf8")); } catch (err) { if (err.code === "ENOENT") return fallback; throw err; }
Show solution
JavaScript Illustrative
import { readFile } from 'node:fs/promises';


async function readJson(file, fallback) {
  try {
    return JSON.parse(await readFile(file, 'utf8'));
  } catch (err) {
    if (err.code === 'ENOENT') return fallback;
    throw err;
  }
}


console.log(await readJson('./does-not-exist.json', { items: [] }));   // { items: [] }

Mini quiz#

File System quizMultiple choice Question 1 of 3

Which error code means the file does not exist?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.