File System with fs/promises
Browsers are not allowed to touch your hard disk, but Node.js can. The built-in fs (file system) module lets your programs create, read, change and delete files and folders. You will use it to save uploaded photos, write log files, read configuration and store small amounts of data as JSON.
In this lesson you will use the modern promise API, node:fs/promises, for everyday file work. You will also see the older callback and synchronous APIs, learn which errors to expect, and read a large file one line at a time.
Three versions of every function
The fs module offers most operations in three styles:
| Style | Import | Example | Use it for |
|---|---|---|---|
| Promise (modern) | node:fs/promises | await readFile(path, "utf8") | Almost everything |
| Callback (original) | node:fs | readFile(path, "utf8", (err, data) => {}) | Old code |
| Synchronous | node:fs | readFileSync(path, "utf8") | Short scripts and startup code |
Synchronous functions end in Sync and block the event loop until the disk finishes. In a small script that is fine. Inside a web server it is a serious mistake, because every user waits while one file is read. This course uses the promise API.
Writing, appending and reading text
writeFile() creates a file, or replaces it if it already exists. appendFile() adds to the end. readFile() returns the whole content.
import { writeFile, appendFile, readFile } from "node:fs/promises";
await writeFile("diary.txt", "26 Jan: Watched the parade in Delhi.\n", "utf8");
await appendFile("diary.txt", "27 Jan: Practised Node.js for one hour.\n", "utf8");
const text = await readFile("diary.txt", "utf8");
console.log(text);
console.log(`Lines: ${text.trim().split("\n").length}`);26 Jan: Watched the parade in Delhi. 27 Jan: Practised Node.js for one hour. Lines: 2
The "utf8" argument is the encoding: it tells Node.js to turn the raw bytes into a string. Without it, readFile() returns a Buffer (raw bytes), which you will meet in Buffers & Streams.
Relative paths such as "diary.txt" are relative to the folder you ran node from, which is called the current working directory, not to the folder of your script. To read a file that sits next to your code, build the path from import.meta.dirname, as the JSON example below does.
Folders: create, list, copy, rename and delete
import { mkdir, writeFile, readdir, stat, rename, copyFile, rm } from "node:fs/promises";
await mkdir("reports/2026/january", { recursive: true });
await writeFile("reports/2026/january/sales.csv", "item,amount\nChai,20\nSamosa,15\n");
await copyFile("reports/2026/january/sales.csv", "reports/2026/january/sales-backup.csv");
await rename("reports/2026/january/sales-backup.csv", "reports/2026/backup.csv");
const entries = await readdir("reports/2026", { withFileTypes: true });
for (const entry of entries) {
console.log(`${entry.isDirectory() ? "[folder]" : "[file] "} ${entry.name}`);
}
const info = await stat("reports/2026/backup.csv");
console.log(`backup.csv is ${info.size} bytes, file: ${info.isFile()}`);
const all = await readdir("reports", { recursive: true });
console.log(all.sort());
await rm("reports", { recursive: true, force: true });
console.log("Removed the reports folder");[file] backup.csv [folder] january backup.csv is 30 bytes, file: true [ '2026', '2026\\backup.csv', '2026\\january', '2026\\january\\sales.csv' ] Removed the reports folder
What each call does:
mkdir(path, { recursive: true })creates the folder and any missing parent folders. Withoutrecursiveit fails if a parent is missing or the folder already exists.copyFile(from, to)copies a file;rename(from, to)renames or moves it.readdir(path, { withFileTypes: true })lists a folder and tells you which entries are folders. Addrecursive: trueto include every sub-folder.stat(path)returns details such assizein bytes,mtime(last modified time),isFile()andisDirectory().rm(path, { recursive: true, force: true })deletes a folder and everything in it.forcemeans "no error if it does not exist". Be careful: there is no recycle bin.
The output above is from Windows, which separates folders with a backslash (shown doubled inside a string). On macOS and Linux you see '2026/backup.csv'. The path & URL Modules lesson shows how to handle both.
Handling file errors
File operations fail for normal reasons: the file is missing, already exists, or you lack permission. Every error has a code property you can check:
import { readFile, writeFile, mkdir } from "node:fs/promises";
try {
await readFile("no-such-file.txt", "utf8");
} catch (err) {
console.log(err.code);
}
await writeFile("once.txt", "first");
try {
await writeFile("once.txt", "second", { flag: "wx" }); // x = fail if it exists
} catch (err) {
console.log(err.code);
}
try {
await mkdir("diary.txt"); // a file with this name already exists
} catch (err) {
console.log(err.code);
}ENOENT EEXIST EEXIST
| Code | Meaning |
|---|---|
ENOENT | No such file or folder |
EEXIST | It already exists |
EACCES or EPERM | Permission denied (or on Windows, the file is open in another program) |
EISDIR | You used a folder where a file was expected |
ENOTDIR | You used a file where a folder was expected |
Do not check whether a file exists and then open it. Another program could delete it in between. Just try the operation and handle ENOENT, as the next example does.
Saving data as JSON
A JSON file is a simple way to store small amounts of data, such as settings or a list of books:
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const FILE = path.join(import.meta.dirname, "books.json");
async function loadBooks() {
try {
return JSON.parse(await readFile(FILE, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return []; // first run: no file yet
throw err;
}
}
async function saveBooks(books) {
await writeFile(FILE, JSON.stringify(books, null, 2) + "\n");
}
const books = await loadBooks();
books.push({ title: "Wings of Fire", author: "A. P. J. Abdul Kalam", price: 299 });
await saveBooks(books);
console.log(`Saved ${books.length} book(s)`);Saved 1 book(s)
Run it again and it prints Saved 2 book(s). JSON.stringify(books, null, 2) indents the file by two spaces so people can read it. For real apps with many users writing at once, use a database instead; you will connect to MySQL and MongoDB later in the course.
Advanced: reading a big file line by line
readFile() loads the whole file into memory. That is fine for a few megabytes, but a 2 GB log file would use 2 GB of memory. Instead, open the file and read it one line at a time with readLines():
import { open } from "node:fs/promises";
const file = await open("server.log");
let errors = 0;
for await (const line of file.readLines()) {
if (line.includes("ERROR")) {
errors++;
console.log(line);
}
}
console.log(`${errors} errors found`);2026-10-30 10:05 ERROR Database timeout 2026-10-30 10:09 ERROR Payment failed for order 1042 2 errors found
open() returns a file handle, an object that represents an open file. readLines() reads it in small pieces and closes it when the loop ends. Memory use stays small however large the file is.
Common mistakes
- Forgetting the encoding.
await readFile("a.txt")gives aBuffer, and printing it shows<Buffer 48 65 ...>. Pass"utf8". - Using
*Syncfunctions inside a server. They block every request. Usenode:fs/promises. - Assuming paths are relative to the script. They are relative to the current working directory. Use
import.meta.dirnamefor files next to your code. - Not awaiting writes. Reading a file straight after a write without
awaitmay read old content. - Deleting with
rmand a wrong path. Double-check paths, especially ones built from user input.
Key points
- Use
node:fs/promiseswithawait; keep*Syncfunctions for short scripts and startup. writeFilereplaces,appendFileadds,readFilereads; pass"utf8"to get text.mkdirwithrecursive: true,readdir,stat,copyFile,renameandrmmanage files and folders.- Errors have codes such as
ENOENTandEEXIST; try the operation and handle the error instead of checking first. - Read huge files line by line with
open()andreadLines()to save memory.
Exercise
Write attendance.js. It should create a folder attendance if it does not exist, then append a line such as 2026-10-30,Isha,present to attendance/october.csv. After appending, read the file and print how many lines it has. Run the program three times and check that the count goes up.
Show solution
// attendance.js
import { mkdir, appendFile, readFile } from "node:fs/promises";
await mkdir("attendance", { recursive: true });
await appendFile("attendance/october.csv", "2026-10-30,Isha,present\n", "utf8");
const text = await readFile("attendance/october.csv", "utf8");
const lines = text.trim().split("\n");
console.log(`Records: ${lines.length}`);Records: 1
mkdir with recursive: true does nothing if the folder already exists, and appendFile creates the file on the first run. The second run prints Records: 2, and the third prints Records: 3.
Quick Quiz
1. Which import gives you the modern promise-based file functions?
The promise API lives in node:fs/promises; its functions return promises you can await.
2. What does the error code ENOENT mean?
ENOENT means there is no entry with that name.
3. Why should you avoid readFileSync() inside a request handler?
Synchronous calls stop the single JavaScript thread until the disk work finishes.
4. How can you read a 3 GB log file without loading it all into memory?
readLines() reads the file in small chunks, one line at a time.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
