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
writeFileSyncoverwrites or creates.appendFileSyncappends to end.- Neither adds
\nautomatically.
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.