Warm-up · Activity 1 of 7
Warm-up from B3.2: docs is a folder, not a file. What does this program print?
import {readFile} from 'node:fs/promises';
try {
await readFile('docs', 'utf8');
} catch (error) {
console.log(error.code);
}// B3.4 · ~33 min · Beginner
After this lesson you can create, list and remove folders, read the size, type and time of an entry with stat, and handle folder errors by their code.
You will be able to
Warm-up · Activity 1 of 7
import {readFile} from 'node:fs/promises';
try {
await readFile('docs', 'utf8');
} catch (error) {
console.log(error.code);
}Predict · Activity 2 of 7
import {readdir} from 'node:fs/promises';
const names = await readdir('docs');
console.log(names.sort().join(', '));Practice · Activity 3 of 7
const entries = await readdir("docs", {____: true});
console.log(entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().join(", "));Practice · Activity 4 of 7
Practice · Activity 5 of 7
import {stat, writeFile} from 'node:fs/promises';
const text = 'Grüße\n';
await writeFile('greeting.txt', text);
const info = await stat('greeting.txt');
console.log(text.length, info.size);Brain teaser · Activity 6 of 7
import {stat, symlink, writeFile} from 'node:fs/promises';
await writeFile('big.txt', 'x'.repeat(1000));
await symlink('big.txt', 'link.txt');
const info = await stat('link.txt');
console.log(info.isSymbolicLink(), info.size);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
This program builds a small folder tree called demo next to main.js, looks at it in three ways, and removes it again. mkdir creates three levels at once and reports the first folder it created. readdir with withFileTypes and stat describe the top level, sorted by name; readdir with recursive lists every path below. rmdir refuses the folder notes because it is not empty, rm with recursive removes it, and stat with throwIfNoEntry: false confirms that it is gone. The program never prints a time, so its output is the same on every run.
main.js
import {mkdir, readdir, rm, rmdir, stat, writeFile} from 'node:fs/promises';
import {join, relative} from 'node:path';
const root = join(import.meta.dirname, 'demo');
await rm(root, {recursive: true, force: true}); // start clean, even if demo is missing
const first = await mkdir(join(root, 'notes', '2026'), {recursive: true});
console.log('first folder created:', relative(import.meta.dirname, first));
await writeFile(join(root, 'todo.txt'), 'buy milk\n');
await writeFile(join(root, 'README.md'), '# Demo\n');
await writeFile(join(root, 'notes', '2026', 'jan.md'), 'Grüße\n');
// The top level: Dirent for the type, stat for the size. Sorted by name.
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
for (const entry of (await readdir(root, {withFileTypes: true})).sort(byName)) {
if (entry.isDirectory()) console.log(` ${entry.name}/`);
else console.log(` ${entry.name} ${(await stat(join(root, entry.name))).size} bytes`);
}
// Every path below demo, sorted.
console.log('all:', (await readdir(root, {recursive: true})).sort().join(', '));
try {
await rmdir(join(root, 'notes'));
} catch (error) {
console.log('rmdir notes:', error.code);
}
await rm(join(root, 'notes'), {recursive: true});
console.log('notes after rm:', await stat(join(root, 'notes'), {throwIfNoEntry: false}));
await rm(root, {recursive: true});
Run it with
node main.jsOutput
first folder created: demo
README.md 7 bytes
notes/
todo.txt 9 bytes
all: README.md, notes, notes/2026, notes/2026/jan.md, todo.txt
rmdir notes: ENOTEMPTY
notes after rm: undefinedExercise 1 of 2
readdir and stat give you records like {name: "img", size: 4096, isDirectory: true}. Write summarize(entries), which returns the lines of a listing: folders first, as img/, then files, as notes.txt (120 bytes), each group sorted by name as sort() sorts strings, and last a line such as 1 folders, 2 files, 150 bytes. Count only the files' bytes: a folder's size is not its contents. This part is pure, 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.
Split the list in two with filter: entries.filter((e) => e.isDirectory) and the rest.
Sort each part with a compare function: (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0).
Add up the sizes of the files only, with reduce((sum, f) => sum + f.size, 0).
One way to solve it. Yours can look different and still pass the checks.
// entries: [{name, size, isDirectory}], as readdir and stat describe them.
// Folders first, then files, each sorted by name; a summary line last.
export function summarize(entries) {
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
const folders = entries.filter((e) => e.isDirectory).sort(byName);
const files = entries.filter((e) => !e.isDirectory).sort(byName);
const bytes = files.reduce((sum, f) => sum + f.size, 0);
return [
...folders.map((f) => f.name + '/'),
...files.map((f) => f.name + ' (' + f.size + ' bytes)'),
folders.length + ' folders, ' + files.length + ' files, ' + bytes + ' bytes'
];
}
const entries = [
{name: 'notes.txt', size: 120, isDirectory: false},
{name: 'img', size: 4096, isDirectory: true},
{name: 'README.md', size: 30, isDirectory: false}
];
console.log(summarize(entries).join('\n'));
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
// entries: [{name, size, isDirectory}], as readdir and stat describe them.
// Folders first, then files, each sorted by name; a summary line last.
export function summarize(entries) {
return entries.map((e) => e.name);
}
const entries = [
{name: 'notes.txt', size: 120, isDirectory: false},
{name: 'img', size: 4096, isDirectory: true},
{name: 'README.md', size: 30, isDirectory: false}
];
console.log(summarize(entries).join('\n'));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {summarize} from './main.js';
const entries = [
{name: 'notes.txt', size: 120, isDirectory: false},
{name: 'img', size: 4096, isDirectory: true},
{name: 'README.md', size: 30, isDirectory: false}
];
test('folders first, then files, each sorted by name', () => {
const got = summarize(entries).slice(0, 3);
assert.deepEqual(got, ['img/', 'README.md (30 bytes)', 'notes.txt (120 bytes)'], `the first lines were ${JSON.stringify(got)}`);
});
test('the last line counts the folders, the files and their bytes', () => {
const got = summarize(entries).at(-1);
assert.equal(got, '1 folders, 2 files, 150 bytes', `the last line was ${JSON.stringify(got)}`);
});
test('two folders are sorted too', () => {
const got = summarize([{name: 'src', size: 4096, isDirectory: true}, {name: 'docs', size: 4096, isDirectory: true}]);
assert.deepEqual(got, ['docs/', 'src/', '2 folders, 0 files, 0 bytes'], `summarize returned ${JSON.stringify(got)}`);
});
test('an empty folder gives only the summary line', () => {
const got = summarize([]);
assert.deepEqual(got, ['0 folders, 0 files, 0 bytes'], `summarize([]) returned ${JSON.stringify(got)}`);
});
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
Write describe(dir), which reads the folder dir and resolves to one record {name, size, isDirectory} per entry, sorted by name. Use readdir with withFileTypes for the type and stat with join(dir, entry.name) for the size. A missing folder (ENOENT) resolves to an empty list; any other error, such as ENOTDIR for a file, must reject. The code below builds a sample folder and prints it. 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.
readdir(dir, {withFileTypes: true}) gives Dirent objects: entry.name and entry.isDirectory().
In the catch block, return [] only when error.code is ENOENT, and throw error otherwise.
stat(join(dir, entry.name)).size is the size; sort the records with a compare function on name.
One way to solve it. Yours can look different and still pass the checks.
import {mkdir, readdir, rm, stat, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
// Describes the entries of dir as {name, size, isDirectory}, sorted by name.
// A missing dir gives an empty list; every other error is thrown again.
export async function describe(dir) {
let entries;
try {
entries = await readdir(dir, {withFileTypes: true});
} catch (error) {
if (error.code === 'ENOENT') return [];
throw error;
}
const records = [];
for (const entry of entries) {
const info = await stat(join(dir, entry.name));
records.push({name: entry.name, size: info.size, isDirectory: entry.isDirectory()});
}
return records.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
// A sample folder next to main.js, made afresh on every run.
const sample = join(import.meta.dirname, 'sample');
await rm(sample, {recursive: true, force: true});
await mkdir(join(sample, 'img'), {recursive: true});
await writeFile(join(sample, 'todo.txt'), 'buy milk\n');
await writeFile(join(sample, 'README.md'), '# Sample\n');
for (const r of await describe(sample)) console.log(r.isDirectory ? r.name + '/' : r.name + ' ' + r.size + ' bytes');
console.log('missing:', await describe(join(import.meta.dirname, 'missing')));
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 {mkdir, readdir, rm, stat, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
// Describes the entries of dir as {name, size, isDirectory}, sorted by name.
// A missing dir gives an empty list; every other error is thrown again.
export async function describe(dir) {
let names;
try {
names = await readdir(dir);
} catch {
return [];
}
return names.map((name) => ({name, size: 0, isDirectory: false}));
}
// A sample folder next to main.js, made afresh on every run.
const sample = join(import.meta.dirname, 'sample');
await rm(sample, {recursive: true, force: true});
await mkdir(join(sample, 'img'), {recursive: true});
await writeFile(join(sample, 'todo.txt'), 'buy milk\n');
await writeFile(join(sample, 'README.md'), '# Sample\n');
for (const r of await describe(sample)) console.log(r.isDirectory ? r.name + '/' : r.name + ' ' + r.size + ' bytes');
console.log('missing:', await describe(join(import.meta.dirname, 'missing')));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {describe} from './main.js';
test('prints the sample folder sorted by name, with the file sizes', async () => {
const got = (await runMain()).trimEnd().split('\n').slice(0, 3);
assert.deepEqual(got, ['README.md 9 bytes', 'img/', 'todo.txt 9 bytes'], `the program printed ${JSON.stringify(got)}`);
});
test('a missing folder gives an empty list', async () => {
const last = (await runMain()).trimEnd().split('\n').at(-1);
assert.equal(last, 'missing: []', `the last line was ${JSON.stringify(last)}`);
});
test('a file instead of a folder rejects with ENOTDIR', async () => {
await assert.rejects(describe(new URL('./main.js', import.meta.url)), {code: 'ENOTDIR'}, 'describe on main.js should reject with the code ENOTDIR');
});
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.jsimport {readdir} from 'node:fs/promises';
for (const entry of await readdir('.')) {
if (entry.isDirectory()) console.log(entry + '/');
}
What Node.js prints
TypeError: entry.isDirectory is not a functionWhy, and the fix
Without options, readdir resolves to strings, and a string has no isDirectory. Ask for Dirent objects: readdir(".", {withFileTypes: true}), then use entry.name for the name and entry.isDirectory() for the type.
import {mkdir, rmdir, writeFile} from 'node:fs/promises';
await mkdir('data', {recursive: true});
await writeFile('data/notes.json', '[]\n');
await rmdir('data');
What Node.js prints
Error: ENOTEMPTY: directory not empty, rmdir 'data'Why, and the fix
rmdir removes only an empty folder. To remove a folder with everything in it, use rm("data", {recursive: true}); add force: true if a missing folder should not be an error either. Check that the path is the one you mean: recursive removal does not ask.
import {mkdir, rm, writeFile} from 'node:fs/promises';
await mkdir('data', {recursive: true});
await writeFile('data/notes.json', '[]\n');
await rm('data');
What Node.js prints
SystemError [ERR_FS_EISDIR]: Path is a directory: rm returned EISDIR (is a directory) dataWhy, and the fix
Without options, rm removes files only, and a folder is refused. Note the code: error.code is ERR_FS_EISDIR here, not EISDIR. Pass {recursive: true} to remove the folder and its contents.
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.