Read and write files in Node.js with fs/promises
A complete Node script that walks a folder, reads text files, and writes a JSON report safely, showing the everyday fs/promises calls and how to handle their errors.
Node’s node:fs/promises module gives you promise-based file functions that work naturally with await. This script uses the ones you will need most often: list a folder, read a file, get file info, create folders and write a file.
What the script does
- Lists every file under a folder, including subfolders, skipping
node_modules. - Records each file’s size and modification time, and counts lines for common text formats.
- Writes everything to a JSON report, using a temporary file and a rename so a crash never leaves a half-written report.
- Prints the five largest files.
The script
Save as dir-report.mjs. Requires Node 20.12 or later (for readdir with recursive and Dirent.parentPath).
#!/usr/bin/env node
// Usage: node dir-report.mjs <folder> [report.json]
import { readdir, readFile, stat, writeFile, rename, mkdir } from "node:fs/promises";
import path from "node:path";
const [folder = ".", output = "report.json"] = process.argv.slice(2);
const TEXT_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".json", ".md", ".txt", ".css", ".html"]);
async function buildReport(root) {
const entries = await readdir(root, { recursive: true, withFileTypes: true });
const files = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
const fullPath = path.join(entry.parentPath, entry.name);
if (fullPath.split(path.sep).includes("node_modules")) continue;
const { size, mtime } = await stat(fullPath);
const ext = path.extname(entry.name).toLowerCase();
let lines = null;
if (TEXT_EXTENSIONS.has(ext)) {
const text = await readFile(fullPath, "utf8");
lines = text === "" ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
}
files.push({ file: path.relative(root, fullPath), ext, size, lines, modified: mtime.toISOString() });
}
files.sort((a, b) => b.size - a.size);
return {
root: path.resolve(root),
generatedAt: new Date().toISOString(),
fileCount: files.length,
totalBytes: files.reduce((sum, f) => sum + f.size, 0),
files,
};
}
async function writeJsonAtomic(file, data) {
await mkdir(path.dirname(path.resolve(file)), { recursive: true });
const tmp = `${file}.${process.pid}.tmp`;
await writeFile(tmp, JSON.stringify(data, null, 2) + "\n", "utf8");
await rename(tmp, file);
}
try {
const report = await buildReport(folder);
await writeJsonAtomic(output, report);
console.log(`Wrote ${output}: ${report.fileCount} files, ${report.totalBytes} bytes`);
for (const f of report.files.slice(0, 5)) {
console.log(` ${String(f.size).padStart(8)} ${f.file}`);
}
} catch (err) {
if (err.code === "ENOENT") console.error(`Not found: ${err.path}`);
else if (err.code === "EACCES") console.error(`Permission denied: ${err.path}`);
else console.error(err);
process.exitCode = 1;
}
Run it
node dir-report.mjs ./src reports/src.json
# Wrote reports/src.json: 42 files, 183220 bytes
The calls, one by one
readdir(dir, { recursive: true, withFileTypes: true })returnsDirentobjects so you can askisFile()without an extrastatcall.readFile(path, "utf8")returns a string. Without the encoding you get aBufferof raw bytes, which is what you want for images.stat(path)returns size, timestamps and type information.mkdir(dir, { recursive: true })creates missing parent folders and does not fail if the folder already exists.writeFile(path, data)creates or replaces a file. UseappendFileto add to the end instead.rename(from, to)replaces the target in one step on the same file system, which is why it is used for the atomic write.
Handling errors
File functions reject with an Error that has a code property. Check that code instead of parsing the message:
ENOENT: the file or folder does not exist.EACCES/EPERM: permission denied.EISDIR: you tried to read a folder as a file.EEXIST: the target exists, for example withwriteFile(path, data, { flag: "wx" }).
To check whether a file exists, prefer simply trying to read it and handling ENOENT. A separate existence check can become wrong between the check and the read.
Tips
- Build paths with
path.join()andpath.resolve(), never by gluing strings with/, so the script also works on Windows. - Relative paths are resolved against the current working directory, not the script’s folder. Use
import.meta.dirname(Node 20.11+) for paths next to the script. readFileloads the whole file into memory. For multi-gigabyte logs, usefs.createReadStreamwithnode:readlineto process line by line.
See also
- 0022 Build a small Node.js CLI with util.parseArgs A complete, dependency-free command-line tool in Node: options and flags with parseArgs, input from files or a pipe, help text, and meaningful… Intermediate · How-to · Any system Script Node.js 15 Oct 2026
- 0027 Fix: SyntaxError: Cannot use import statement outside a module The file uses ES module import syntax, but Node or the browser is loading it as a classic script or CommonJS. Tell… Intermediate · Fix · Any system Article Common errors · Node.js 15 Oct 2026
- 0263 Build a notes REST API with plain Node.js: CRUD, validation, status codes and CORS Write a small JSON API for notes using only Node.js built-ins: create, read, update and delete, input validation, correct HTTP status codes,… Intermediate · Tutorial · Any system Article Making requests · Node.js · Status codes 15 Oct 2026
Repos in the wild
- Node.js nodejs/node The source of the Node.js runtime: a JavaScript engine (V8) plus APIs for files, networking, processes and more, used to run JavaScript outside the browser. GitHub
- Express (expressjs/express) expressjs/express A minimal and flexible web framework for Node.js, built around routes and middleware, and the foundation of countless APIs and other frameworks. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- commitpedia-admin +4,766 chars