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.
On this page · 6 min · Beginner
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Beginner
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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');
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#
- mkdtemp creates a unique temporary folder so the example never touches your real files.
- JSON is stringified (pretty-printed with 2 spaces) before writing and parsed after reading.
- appendFile adds lines without rewriting the file.
- readdir and stat inspect the folder and file size.
- A missing file throws an error whose code is ENOENT.
- 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#
readFileSync on a 50 MB file freezes the server. Use the async API or streams.
path.join(uploads, req.query.file) with "../../.env" escapes the folder. Validate names and check the resolved path stays inside the base folder.
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?
Show solution
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#
Which error code means the file does not exist?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.