Skip to content
wannacode

Files

Problem

The program forgets

You are writing a program for your expenses. You enter them in the evening and close it, and the next day it knows nothing about yesterday. Here is the shortest example:

let visits = 0;
visits++;
console.log(`Started ${visits} time.`);
Output
Started 1 time.

No matter how many times you run it, it says "1 time". Variables live in memory while the program runs. When it ends, the memory is freed and everything disappears.

To remember, the program has to write the data where it stays after the program ends: in a file on the disk.

The idea

A file with node:fs

Here is a whole program that writes a note to a file and reads it back:

notes.js
import { readFileSync, writeFileSync } from "node:fs";
try {
writeFileSync("notes.txt", "Buy milk\nPay the power bill by the 15th\n");
const text = readFileSync("notes.txt", "utf8");
process.stdout.write(text);
} catch (e) {
console.log(`Problem with the file: ${e.message}`);
}
Output
Buy milk
Pay the power bill by the 15th
  • import { readFileSync, writeFileSync } from "node:fs"; takes two functions from the node:fs module (file system). It comes with Node and you do not install it.
  • writeFileSync(name, text) writes the text and creates the file if it is not there. "notes.txt" is a file with this name in the folder the program was started from.
  • readFileSync(name, "utf8") reads the whole file as text. "utf8" is the encoding: how bytes become letters.
  • "Sync" means that the program waits until the file is written or read, and only then carries on.
  • If something goes wrong (the disk is full, the file is locked or missing), the function throws an error. JavaScript does not make you catch it, but without catch the program stops.
  • process.stdout.write(text) prints the text as it is. console.log would add one more new line at the end.

Run the program, then open the folder: notes.txt is there, and you can open it with any text editor. The next run will find it.

In every example from here on, the imports are at the top: from the module you take only the functions you use.

The idea

Add and read the lines

Expenses are added one by one. appendFileSync adds at the end, instead of writing the file from scratch. split("\n") splits the text you read into lines:

import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
try {
writeFileSync("expenses.txt", "Lunch;12.50\n");
appendFileSync("expenses.txt", "Taxi;8.20\n");
appendFileSync("expenses.txt", "Book;18.90\n");
const lines = readFileSync("expenses.txt", "utf8").trimEnd().split("\n");
let total = 0;
for (const line of lines) {
const parts = line.split(";");
total += Number(parts[1]);
console.log(`${parts[0]}: €${parts[1]}`);
}
console.log(`${lines.length} expenses, total €${total.toFixed(2)}`);
} catch (e) {
console.log(`Problem with the file: ${e.message}`);
}
Output
Lunch: €12.50
Taxi: €8.20
Book: €18.90
3 expenses, total €39.60

lines is an array of strings, one for each line. trimEnd() before split removes the last new line character. Without it, the array would have one more element at the end: an empty string "".

More precisely, if you are curious
  • appendFileSync creates the file if it is not there, so the first append gives no error.
  • Without "utf8", readFileSync returns a Buffer: the bytes of the file themselves, for example <Buffer 43 61 66 c3 a9 ...>. It becomes text only with the encoding.
  • There is also a version where the program does not stop and wait for the disk: import { readFile } from "node:fs/promises"; and const text = await readFile("expenses.txt", "utf8");. Servers use that one. In short programs, readFileSync is simpler.
  • readFileSync reads the whole file into memory. For a file of several gigabytes it is better to read line by line. See the card on streams.

Try it

Two shifts

What will the program print?

import { readFileSync, writeFileSync } from "node:fs";
try {
writeFileSync("shift.txt", "First shift: Ivo\n");
writeFileSync("shift.txt", "Second shift: Mila\n");
process.stdout.write(readFileSync("shift.txt", "utf8"));
} catch (e) {
console.log(e.message);
}
Answer

The idea

When the file is not there

The program's settings are in config.txt, and someone deleted it. If nobody catches the exception:

app.js
import { readFileSync } from "node:fs";
function readConfig() {
return readFileSync("config.txt", "utf8");
}
console.log(readConfig());
Error
node:fs:440
return binding.readFileUtf8(path, stringToFlags(options.flag));
^
Error: ENOENT: no such file or directory, open 'config.txt'
at readFileSync (node:fs:440:20)
at readConfig (file:///app/app.js:4:10)
at file:///app/app.js:7:13
... {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'config.txt'
}

At the top, Node shows the line from its own internal code where the error was thrown. Then comes the error itself: ENOENT (error, no entry) is the code for "no such file or directory". The lines with "at" go from the inside out: readFileSync, then readConfig on line 4, called from line 7. The lines of Node after them are shortened with "...".

The errors of node:fs are of the kind Error, and they differ by e.code. Check the code in catch:

import { existsSync, readFileSync } from "node:fs";
function readConfig() {
return readFileSync("config.txt", "utf8");
}
try {
console.log(readConfig());
} catch (e) {
if (e.code === "ENOENT") {
console.log(`No file ${e.path}. Using the default settings.`);
} else {
console.log(`Cannot read the settings: ${e.message}`);
}
}
console.log(existsSync("config.txt"));
Output
No file config.txt. Using the default settings.
false

e.path is the name of the missing file. existsSync(path) checks whether the file exists. The check does not replace catch: the file may disappear between the check and the reading, and there are other reasons the reading may fail.

The idea

Line by line and streams

writeFileSync writes the whole text at once. For writing in parts and for reading line by line, there are streams. A stream keeps the file open while you work with it, so it has to be closed at the end:

import { createReadStream, createWriteStream } from "node:fs";
import { once } from "node:events";
import { createInterface } from "node:readline";
try {
const out = createWriteStream("report.txt");
for (let day = 1; day <= 3; day++) {
out.write(`Day ${day}: €${day * 120}\n`);
}
out.end();
await once(out, "close");
} catch (e) {
console.log(`Cannot write: ${e.message}`);
}
try {
const lines = createInterface({ input: createReadStream("report.txt") });
for await (const line of lines) {
console.log(line);
}
} catch (e) {
console.log(`Cannot read: ${e.message}`);
}
Output
Day 1: €120
Day 2: €240
Day 3: €360
  • createWriteStream opens the file for writing. write gives the text to the stream. A new line does not come by itself: you write the \n.
  • end() says "that is all" and starts the closing. await once(out, "close") waits until the stream writes everything and closes the file. once is from the node:events module.
  • createInterface from node:readline reads the stream from the file line by line, and for await gives the lines one by one, without the new line character. This is the same thing you use to read from the keyboard.

Closing is not a formality: until the stream is closed, the text may still be in its buffer in memory, not on the disk. To keep the code short, there is no try here:

import { createWriteStream, readFileSync, statSync } from "node:fs";
import { once } from "node:events";
const out = createWriteStream("report.txt");
for (let day = 1; day <= 3; day++) {
out.write(`Day ${day}: €${day * 120}\n`);
}
console.log(`In the buffer: ${out.writableLength} bytes`);
out.end();
await once(out, "close");
console.log(`In the file: ${statSync("report.txt").size} bytes`);
for (const line of readFileSync("report.txt", "utf8").trimEnd().split("\n")) {
console.log(line);
}
Step by step, in words
  1. node:fs gives the stream and the size of the file, node:events the waiting. On the left is the memory of the program, on the right is the disk.
  2. createWriteStream creates a stream with a buffer in memory. The file report.txt opens without the program waiting.
  3. write gives the text to the stream, and it waits in the buffer. Nothing reaches the disk yet.
  4. writableLength: 42 bytes wait in the buffer. Not a single one is on the disk.
  5. end() closes the stream, and await waits for that to happen. The buffer goes to the disk: the file becomes 42 bytes.
  6. 42 bytes for 36 characters with the new lines: in UTF-8 each Latin letter is 1 byte, and € is 3.
  7. readFileSync reads the three lines back from the disk.

JavaScript has no try (...) that closes a resource by itself. readFileSync and writeFileSync do not need one, because they open and close the file themselves. When you keep the file open, the closing goes in finally, from the lesson on finally:

import { closeSync, openSync, writeSync } from "node:fs";
const fd = openSync("report.txt", "w");
try {
writeSync(fd, "Day 1: €120\n");
} finally {
closeSync(fd);
}

openSync opens the file and returns its number (fd, file descriptor). writeSync writes straight to the file, with no buffer. closeSync closes it, whatever happens in the try.

More precisely, if you are curious
  • The language is getting using: a resource that closes by itself at the end of the block, like try (...) in Java. Node 22 does not recognize it yet and gives a SyntaxError.
  • Buffer. The stream collects the text in memory (out.writableLength is how many bytes are waiting) and sends it to the disk without the program stopping to wait. That is why you need an await after end().
  • readline over createReadStream is the way for a very big file: only the current chunk is in memory. Here it is on its own:
import { createReadStream, writeFileSync } from "node:fs";
import { createInterface } from "node:readline";
writeFileSync("report.txt", "Day 1: €120\nDay 2: €240\n");
const lines = createInterface({ input: createReadStream("report.txt") });
for await (const line of lines) {
console.log(line);
}

Try it

When does it close?

JavaScript has no try (...), so the resource is closed in finally. Here is the receipt printer from the lesson on finally: close() is in the finally of the inner try, and the error is caught by the outer one:

What will the program print?

class Printer {
constructor() {
console.log("The printer is busy.");
}
close() {
console.log("The printer is free.");
}
}
try {
const printer = new Printer();
try {
console.log("Printing the receipt...");
const qty = Number("three");
if (Number.isNaN(qty)) {
throw new Error("three is not a number");
}
console.log(`Quantity: ${qty}`);
} finally {
printer.close();
}
} catch (e) {
console.log("Wrong quantity.");
}
Answer

Try it

Find the bug

The program adds an order under the header line and counts the lines in the file. It prints 1, but there are two lines. Which line has the bug?

Pick the line with the bug.

The idea

Folders

The register's backups go into a folder, one per day:

import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const backup = "backup";
try {
mkdirSync(join(backup, "old"), { recursive: true });
writeFileSync(join(backup, "2026-10-01.csv"), "Café,2.40\n");
writeFileSync(join(backup, "2026-10-02.csv"), "Café,2.40\nTea,1.80\n");
for (const name of readdirSync(backup).sort()) {
const p = join(backup, name);
if (statSync(p).isDirectory()) {
console.log(`${name}/`);
} else {
console.log(`${name} (${statSync(p).size} bytes)`);
}
}
unlinkSync(join(backup, "2026-10-01.csv"));
console.log(existsSync(join(backup, "2026-10-01.csv")));
} catch (e) {
console.log(e.message);
}
Output
2026-10-01.csv (11 bytes)
2026-10-02.csv (20 bytes)
old/
false
Function What it does
mkdirSync(p, { recursive: true }) creates the folder and all the folders above it, if they are not there
join(dir, "name") (from node:path) the path to name inside dir
statSync(p).isDirectory(), existsSync(p) is it a folder, is it there
statSync(p).size the size in bytes
unlinkSync(p) deletes a file (rmdirSync(p) deletes an empty folder)
readdirSync(dir) an array with the names of everything in the folder

The sizes are 11 and 20 bytes, though there are fewer characters: in UTF-8, the "é" in "Café" is 2 bytes.

readdirSync does not promise an order for the names, so we sort them with sort().
More precisely, if you are curious
  • rmdirSync deletes only an empty folder. Otherwise it throws an error with the code ENOTEMPTY. rmSync(p, { recursive: true }) deletes the folder with everything in it, so be careful with it.
  • copyFileSync(src, dst) copies a file, and renameSync(src, dst) moves or renames it.
  • join puts the system's separator between the parts: / on Linux and macOS, \ on Windows. That is why you do not build paths with +.
  • The path "notes.txt" is relative: to the folder the program was started from. "/home/ana/notes.txt" is absolute. resolve("notes.txt") from node:path shows exactly where the file is.

Exercise

Programs that remember

Solve the tasks in your editor. In each task, the program first creates the input file itself with writeFileSync, so you can try it right away.

1. A configuration file. Create server.conf with these contents, then read it and print each setting as key: value. Skip the empty lines and the lines that start with #. Hint: find the first = with indexOf and take the two parts with slice. split("=", 2) does not help here: in JavaScript it throws away everything after the second =.

server.conf
# server settings
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Output
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. A log file filter. Create app.log with four lines (two INFO and two ERROR). Read a level from the keyboard, for example ERROR, and write the lines with that level to errors.log line by line: read with readline, write with a stream (createWriteStream). Print errors.log and how many lines you wrote.

Input
ERROR
Output
ERROR: Failed to load configuration
ERROR: Null pointer exception
Wrote 2 lines to errors.log

3. Folder contents. Create a folder project with the files document.txt, image.jpg, script.js and the folder backup. Write their names, sorted in alphabetical order, to directory_contents.txt, with a / after the names of folders. Print the file.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. An index file. Create text.txt with the three lines below. Read a word from the keyboard and write to index.txt each place it appears, in the form line:position: the line number (from 1) and the index in the line (from 0). If the word is on one line twice, that is two entries.

text.txt
hello world
world hello
hello again, hello
Input
hello
index.txt
1:0
2:6
3:0
3:13

More tasks

  • Run counter. Fix the program from the first card: count the runs in visits.txt. If the file is not there, start from 0.
  • Find and replace in a file. The task from the lesson "Search and replace": read a file, replace "old" with "new", and write the result to a new file.
  • Dates from a file. The date task from the lesson "Regular expressions": read the dates from a file, one per line.
  • A copy with line numbers. Read a file and write a copy in which each line starts with its number: 1: ..., 2: ....

Test

Check yourself

1. What does readFileSync("notes.txt") return without "utf8"?
2. writeFileSync(file, "A\n"); then writeFileSync(file, "B\n"); What is in the file?
3. Does JavaScript (Node 22) have a try (...) that closes the file by itself?
4. readFileSync cannot find the file. How do you tell that this is the problem?
5. You write with createWriteStream, call end() and right away read the file with readFileSync. What may be in it?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·