Skip to main content

Chapter 10 - Reading and Writing Files (JavaScript)

Here's a JavaScript-flavoured version of the same concepts, with small JS examples for each idea.


Files and filepaths

A file has a filename and a path; paths are made of folders (directories), with a root like C:\ on Windows or / on macOS/Linux.

const path = require("path");

const p = path.resolve("C:/Users/Al/Documents/project.docx");
console.log(path.basename(p)); // 'project.docx'
console.log(path.dirname(p)); // 'C:/Users/Al/Documents'

Standardizing path separators (path.join)

Use path.join() instead of hard-coding \ or /; it normalizes separators per OS.

const path = require("path");

const p = path.join("spam", "bacon", "eggs");
console.log(p); // e.g. 'spam/bacon/eggs'
console.log(String(p)); // OS-native string path

Joining paths with path.join()

path.join() concatenates any number of path segments into one normalized path.

const path = require("path");

const p = path.join("spam", "bacon", "eggs");
console.log(p); // 'spam/bacon/eggs'

Unlike Python's / operator, path.join() works with plain strings on both sides.


Current working directory

process.cwd() gives current working directory; process.chdir() changes it.

console.log(process.cwd());
process.chdir("/tmp");
console.log(process.cwd());

Home directory

os.homedir() returns the user's home folder; good default base for your own files.

const os = require("os");
const path = require("path");

const home = os.homedir();
console.log(home); // e.g. '/Users/YourName'
const config = path.join(home, "myapp", "config.json");

Absolute vs relative paths

  • Absolute starts at root (C:\ or /).
  • Relative is interpreted from current working directory; . = current, .. = parent.
const path = require("path");

console.log(path.isAbsolute("spam/bacon/eggs")); // false
console.log(path.resolve("spam/bacon/eggs")); // absolute version

Creating directories

fs.mkdirSync() with recursive: true creates directories including all parents.

const fs = require("fs");

fs.mkdirSync("delicious/walnut/waffles", { recursive: true });
fs.mkdirSync("spam", { recursive: true });

Path parts: root, dir, base, name, ext

You can pull apart a path into its components with path.parse().

const path = require("path");

const p = "C:/Users/Al/spam.txt";
const parsed = path.parse(p);
console.log(parsed.root); // 'C:/'
console.log(parsed.dir); // 'C:/Users/Al'
console.log(parsed.base); // 'spam.txt'
console.log(parsed.name); // 'spam'
console.log(parsed.ext); // '.txt'

File stats: size and timestamps

fs.statSync() returns size and times (size, mtime, etc.).

const fs = require("fs");

const st = fs.statSync("package.json");
console.log(st.size); // bytes
console.log(st.mtime.toLocaleString()); // modification time

Glob patterns with readdirSync + filter

Node.js has no built-in glob, but you can use fs.readdirSync with a filter, or install the glob package.

const fs = require("fs");
const path = require("path");

const desktop = path.join(require("os").homedir(), "Desktop");
const txtFiles = fs.readdirSync(desktop).filter((f) => f.endsWith(".txt"));
for (const f of txtFiles) {
console.log(path.join(desktop, f));
}

Checking path existence / type

  • fs.existsSync(p) – path exists.
  • fs.statSync(p).isFile() – existing file.
  • fs.statSync(p).isDirectory() – existing directory.
const fs = require("fs");

console.log(fs.existsSync("/usr")); // true
console.log(fs.statSync("/usr").isDirectory()); // true

console.log(fs.existsSync("/bin/ls")); // true
console.log(fs.statSync("/bin/ls").isFile()); // true

Basic text file helpers: readFileSync / writeFileSync

fs.readFileSync() and fs.writeFileSync() are convenience methods for text files.

const fs = require("fs");

fs.writeFileSync("spam.txt", "Hello, world!");
console.log(fs.readFileSync("spam.txt", "utf-8")); // 'Hello, world!'

The three-step file I/O pattern (sync methods)

JavaScript's fs sync methods handle open/read/close internally, so the three-step pattern is implicit.

const fs = require("fs");
const path = require("path");
const os = require("os");

const filePath = path.join(os.homedir(), "hello.txt");
const content = fs.readFileSync(filePath, "utf-8");
console.log(content);

Async version with fs/promises:

const fsp = require("fs/promises");
const content = await fsp.readFile(filePath, "utf-8");

Reading whole file: readFileSync

fs.readFileSync(path, 'utf-8') returns the entire file contents as a single string.

const fs = require("fs");

const text = fs.readFileSync("hello.txt", "utf-8");
console.log(text);

Reading by lines: split on newline

readFileSync gives you one big string; split on "\n" to get lines.

const fs = require("fs");
const path = require("path");
const os = require("os");

const filePath = path.join(os.homedir(), "sonnet29.txt");
const text = fs.readFileSync(filePath, "utf-8");
const lines = text.split("\n");
console.log(lines[0]); // first line

Writing files: write vs append

  • writeFileSync overwrites or creates.
  • appendFileSync appends to end.
  • Neither adds \n automatically.
const fs = require("fs");

// Write (overwrite)
fs.writeFileSync("bacon.txt", "Hello, world!\n", "utf-8");

// Append
fs.appendFileSync("bacon.txt", "Bacon is not a vegetable.", "utf-8");

Using path.join with file I/O

You can pass a joined path directly to readFileSync/writeFileSync.

const fs = require("fs");
const path = require("path");
const os = require("os");

const p = path.join(os.homedir(), "data.txt");
fs.writeFileSync(p, "test", "utf-8");

try/finally for file cleanup (like with)

JavaScript has no with-for-files, but sync methods handle open/close automatically. For streams, use try/finally.

const fs = require("fs");

// Sync methods handle open/close automatically
fs.writeFileSync("data.txt", "Hello, world!", "utf-8");
const content = fs.readFileSync("data.txt", "utf-8");

Persisting data with JSON (like shelve)

JSON is the standard way to persist structured data in JavaScript, giving you dictionary-like storage backed by a file.

const fs = require("fs");

// Save data
const data = { cats: ["Zophie", "Pooka", "Simon"] };
fs.writeFileSync("mydata.json", JSON.stringify(data, null, 2));

Later:

const loaded = JSON.parse(fs.readFileSync("mydata.json", "utf-8"));
console.log(loaded.cats); // ['Zophie', 'Pooka', 'Simon']
console.log(Object.keys(loaded)); // ['cats']

Project: Generate Random Quiz Files

Goal: generate multiple quiz/answer files using file I/O and randomization.

Key ideas:

  • Store data in an object (states → capitals).
  • For each quiz file: randomize question order, write MC questions.
  • For each answer key: write correct letters.

Tiny skeleton:

const fs = require("fs");
const path = require("path");

const capitals = { Alabama: "Montgomery", Alaska: "Juneau" };
const quizDir = "quizzes";
fs.mkdirSync(quizDir, { recursive: true });

function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}

for (let quizNum = 1; quizNum <= 2; quizNum++) {
let quizText = "";
let ansText = "";
const states = shuffle(Object.keys(capitals));

states.forEach((state, idx) => {
const qNum = idx + 1;
const correct = capitals[state];
const wrong = Object.values(capitals).filter((v) => v !== correct);
const options = shuffle([...wrong.slice(0, 3), correct]);

quizText += `${qNum}. What is the capital of ${state}?\n`;
options.forEach((opt, i) => {
quizText += ` ${"ABCD"[i]}. ${opt}\n`;
});
quizText += "\n";
ansText += `${qNum}. ${"ABCD"[options.indexOf(correct)]}\n`;
});

fs.writeFileSync(path.join(quizDir, `capitals_quiz_${quizNum}.txt`), quizText);
fs.writeFileSync(path.join(quizDir, `capitals_quiz_answers_${quizNum}.txt`), ansText);
}

Overall idea of the chapter

This chapter's pattern is: use path for safe paths, fs.readFileSync/fs.writeFileSync for I/O, and JSON for persisting JavaScript objects.