File System with fs/promises

Node.js · Lesson 7 of 30 7 min read Updated

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:

StyleImportExampleUse it for
Promise (modern)node:fs/promisesawait readFile(path, "utf8")Almost everything
Callback (original)node:fsreadFile(path, "utf8", (err, data) => {})Old code
Synchronousnode:fsreadFileSync(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.

JavaScript
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}`);
Output
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

JavaScript
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");
Output
[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. Without recursive it 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. Add recursive: true to include every sub-folder.
  • stat(path) returns details such as size in bytes, mtime (last modified time), isFile() and isDirectory().
  • rm(path, { recursive: true, force: true }) deletes a folder and everything in it. force means "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:

JavaScript
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);
}
Output
ENOENT
EEXIST
EEXIST
CodeMeaning
ENOENTNo such file or folder
EEXISTIt already exists
EACCES or EPERMPermission denied (or on Windows, the file is open in another program)
EISDIRYou used a folder where a file was expected
ENOTDIRYou 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:

JavaScript
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)`);
Output
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():

JavaScript
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`);
Output
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 a Buffer, and printing it shows <Buffer 48 65 ...>. Pass "utf8".
  • Using *Sync functions inside a server. They block every request. Use node:fs/promises.
  • Assuming paths are relative to the script. They are relative to the current working directory. Use import.meta.dirname for files next to your code.
  • Not awaiting writes. Reading a file straight after a write without await may read old content.
  • Deleting with rm and a wrong path. Double-check paths, especially ones built from user input.

Key points

  • Use node:fs/promises with await; keep *Sync functions for short scripts and startup.
  • writeFile replaces, appendFile adds, readFile reads; pass "utf8" to get text.
  • mkdir with recursive: true, readdir, stat, copyFile, rename and rm manage files and folders.
  • Errors have codes such as ENOENT and EEXIST; try the operation and handle the error instead of checking first.
  • Read huge files line by line with open() and readLines() 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
JavaScript
// 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}`);
Output
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?

2. What does the error code ENOENT mean?

3. Why should you avoid readFileSync() inside a request handler?

4. How can you read a 3 GB log file without loading it all into memory?

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