Warm-up · Activity 1 of 7
// B3.2 · ~34 min · Beginner
Reading files with fs/promises
After this lesson you can read text and JSON files, tell a missing file from a folder or a locked one, and find a file next to your module.
Predict · Activity 2 of 7
Predict before you read on. hello.txt holds the line Hi. What does this program print?
import {readFile} from 'node:fs/promises'; const data = await readFile('hello.txt'); console.log(data);Practice · Activity 3 of 7
Fill in the encoding so that readFile resolves to a string, and the program prints string buy milk.
const text = await readFile("notes.txt", "____"); console.log(typeof text, text.trim());const text = await readFile("notes.txt", "");Practice · Activity 4 of 7
Your program reads a file and parses it as JSON. Match each error to the situation that causes it.
Practice · Activity 5 of 7
In which order does this program print its three lines?
import {readFileSync} from 'node:fs'; import {readFile} from 'node:fs/promises'; readFile('names.txt', 'utf8').then(() => console.log('promise')); readFileSync('names.txt', 'utf8'); console.log('sync'); console.log('end');Brain teaser · Activity 6 of 7
Brain teaser. The folder app holds main.js and data.txt, which contains hello from app. You stand in the folder above app and run node app/main.js. What happens?
// app/main.js import {readFile} from 'node:fs/promises'; const text = await readFile('data.txt', 'utf8'); console.log(text.trim());Apply · Activity 7 of 7
Mini-task. Make a folder app with config.json ({"port": 8080}) and a main.js that reads config.json next to itself and prints port 8080. Start it from the folder above with node app/main.js. Then rename config.json, and later replace it with a folder of the same name: each time, the program must print one clear line to stderr and end with exit code 1, without a stack trace.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A task list, and three ways a read can fail
This program reads tasks.txt from the folder of main.js, splits it into lines and skips the blank one. Then it tries a file that does not exist and the folder itself, and turns each error.code into a short sentence; an unexpected code would be thrown again. Last, it reads tasks.txt without an encoding, to show that you get bytes. Because the paths start at import.meta.dirname, the output is the same wherever you run it from.
main.js
import {readFile} from 'node:fs/promises';
import {join} from 'node:path';
const here = import.meta.dirname; // the folder of main.js
async function show(name) {
try {
const text = await readFile(join(here, name), 'utf8');
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== '');
console.log(`${name}: ${lines.length} tasks`);
for (const line of lines) console.log(' -', line);
} catch (error) {
const why = {ENOENT: 'does not exist', EISDIR: 'is a folder', EACCES: 'may not be read'}[error.code];
if (!why) throw error; // not one we expected: let it crash
console.log(`${name}: ${why} (${error.code})`);
}
}
await show('tasks.txt');
await show('missing.txt');
await show('.');
const bytes = await readFile(join(here, 'tasks.txt'));
console.log('without an encoding:', bytes.length, 'bytes', bytes.subarray(0, 3));
tasks.txt
buy milk
water plants
call Ada
Run it with
node main.jsOutput
tasks.txt: 3 tasks
- buy milk
- water plants
- call Ada
missing.txt: does not exist (ENOENT)
.: is a folder (EISDIR)
without an encoding: 32 bytes <Buffer 62 75 79>- The blank line between buy milk and water plants, and the empty item after the last newline, are filtered out.
- join(here, '.') is the folder of main.js itself, so readFile rejects with EISDIR.
- EACCES is in the list, but no file here is locked, so it never shows.
- Without "utf8", readFile gives a Buffer: 62 75 79 are the bytes of b, u and y.
Exercises
Exercise 1 of 2
From file text to records
A program reads scores.txt with readFile(path, "utf8") and hands the text to parseScores(text). Each line is name,score. Return an array of {name, score} objects with score as a number. Skip blank lines, including the empty item after the final newline, accept Windows line ends (\r\n), and trim spaces around both parts. This part needs no file at all, so it runs in the browser too.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Hints
Hint 1
text.split(/\r?\n/) cuts at \n and at \r\n alike.
Hint 2
filter((line) => line.trim() !== "") removes blank lines and the empty last item.
Hint 3
line.split(",") gives [name, score]; trim the name, and Number(" 90") is 90.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// Turns the text of scores.txt (one "name,score" per line) into records.
// readFile(path, "utf8") gives this text; parsing it needs no file.
export function parseScores(text) {
return text
.split(/\r?\n/)
.filter((line) => line.trim() !== "")
.map((line) => {
const [name, score] = line.split(",");
return {name: name.trim(), score: Number(score)};
});
}
console.log(parseScores("Ada,90\nGrace,85\n"));
Run it on your computer
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Turns the text of scores.txt (one "name,score" per line) into records.
// readFile(path, "utf8") gives this text; parsing it needs no file.
export function parseScores(text) {
return [];
}
console.log(parseScores("Ada,90\nGrace,85\n"));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseScores} from './main.js';
test('two lines with a final newline give two records', () => {
const got = parseScores('Ada,90\nGrace,85\n');
assert.deepEqual(got, [{name: 'Ada', score: 90}, {name: 'Grace', score: 85}], `parseScores returned ${JSON.stringify(got)}`);
});
test('blank lines are skipped', () => {
const got = parseScores('Ada,90\n\n\nGrace,85');
assert.equal(got.length, 2, `parseScores returned ${JSON.stringify(got)}`);
});
test('Windows line ends and spaces are handled', () => {
const got = parseScores(' Ada , 90\r\nGrace,85\r\n');
assert.deepEqual(got, [{name: 'Ada', score: 90}, {name: 'Grace', score: 85}], `parseScores returned ${JSON.stringify(got)}`);
});
test('an empty file gives an empty array', () => {
assert.deepEqual(parseScores(''), [], `parseScores("") returned ${JSON.stringify(parseScores(''))}`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
Clear messages for a settings file
loadSettings(name) reads the JSON file name from the folder of main.js and returns the parsed value. Make every expected problem reject with an Error whose message is one clear line: "missing.json: no such file" for ENOENT, ".: is a folder, not a file" for EISDIR (here the name was "."), and "broken.json: not valid JSON" when JSON.parse fails. Throw any other error again unchanged. Run node main.js and check with node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Hints
Hint 1
Wrap only the readFile call in its own try, and JSON.parse in a second one, so you know which step failed.
Hint 2
In the first catch, compare error.code with 'ENOENT' and 'EISDIR', and end with throw error; for the rest.
Hint 3
throw new Error(`${name}: no such file`) rejects the async function with that message.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import {readFile} from 'node:fs/promises';
import {join} from 'node:path';
// Reads a JSON file next to main.js and returns the parsed value.
export async function loadSettings(name) {
let text;
try {
text = await readFile(join(import.meta.dirname, name), 'utf8');
} catch (error) {
if (error.code === 'ENOENT') throw new Error(`${name}: no such file`);
if (error.code === 'EISDIR') throw new Error(`${name}: is a folder, not a file`);
throw error;
}
try {
return JSON.parse(text);
} catch {
throw new Error(`${name}: not valid JSON`);
}
}
try {
console.log(await loadSettings('settings.json'));
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
Run it on your computer
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import {readFile} from 'node:fs/promises';
import {join} from 'node:path';
// Reads a JSON file next to main.js and returns the parsed value.
export async function loadSettings(name) {
const text = await readFile(join(import.meta.dirname, name), 'utf8');
return JSON.parse(text);
}
try {
console.log(await loadSettings('settings.json'));
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadSettings} from './main.js';
test('reads and parses settings.json', async () => {
const got = await loadSettings('settings.json');
assert.deepEqual(got, {theme: 'dark', fontSize: 14}, `loadSettings returned ${JSON.stringify(got)}`);
});
test('a missing file gives one clear message', async () => {
await assert.rejects(loadSettings('missing.json'), {message: 'missing.json: no such file'}, 'expected the message "missing.json: no such file"');
});
test('a folder gives one clear message', async () => {
await assert.rejects(loadSettings('.'), {message: '.: is a folder, not a file'}, 'expected the message ".: is a folder, not a file"');
});
test('broken JSON gives one clear message', async () => {
await assert.rejects(loadSettings('broken.json'), {message: 'broken.json: not valid JSON'}, 'expected the message "broken.json: not valid JSON"');
});
settings.json
{"theme": "dark", "fontSize": 14}
broken.json
{theme: "dark"}
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsCommon mistakes
Parsing the promise instead of the text
import {readFile} from 'node:fs/promises';
const settings = JSON.parse(readFile('settings.json', 'utf8'));
console.log(settings.theme);
What Node.js prints
SyntaxError: Unexpected token 'o', "[object Promise]" is not valid JSONWhy, and the fix
readFile from node:fs/promises returns a promise, not the text. JSON.parse turns its argument into a string first, which gives "[object Promise]", and that is not JSON. Await the read: JSON.parse(await readFile("settings.json", "utf8")).
Treating a Buffer as a string
import {readFile} from 'node:fs/promises';
const text = await readFile('names.txt');
console.log(text.split('\n'));
What Node.js prints
TypeError: text.split is not a functionWhy, and the fix
Without an encoding, readFile resolves to a Buffer of bytes, and a Buffer has no split method. Ask for text when you read: await readFile("names.txt", "utf8"). Then split, trim and the other string methods work.
Ignoring err in a callback
import {readFile} from 'node:fs';
readFile('setings.json', 'utf8', (err, text) => {
console.log(text.trim());
});
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'trim')Why, and the fix
The callback form passes the error first. Here the name is misspelled, so err is an ENOENT error and text is undefined, and the crash hides the real cause. Check err before using the data: if (err) { console.error(err.code); return; }. Or use the promise form with try/catch.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.