Skip to content
aviral gupta

// B3.4 · ~33 min · Beginner

Folders and stat

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.

Lesson 4 of 5 in B3 Files and paths

You will be able to

  • Create, list and remove folders with mkdir, readdir, rmdir and rm, and copy or move them
  • Read size, type and modification time with stat or lstat, and sort listings before printing
  • Try the operation instead of checking first, and handle ENOENT, ENOTDIR, ENOTEMPTY and EEXIST
  1. 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);
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. The folder docs holds a.txt, Beta.md and c.txt. What does this program print?

    import {readdir} from 'node:fs/promises';
    
    const names = await readdir('docs');
    console.log(names.sort().join(', '));
  3. Practice · Activity 3 of 7

    docs holds the file a.txt and the folders img and notes. Fill in the option so that readdir gives Dirent objects and the program prints img, notes.

    const entries = await readdir("docs", {____: true});
    console.log(entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().join(", "));
    const entries = await readdir("docs", {: true});
  4. Practice · Activity 4 of 7

    The folder full holds a.txt, and there is no missing. Match each call to what happens.

  5. Practice · Activity 5 of 7

    What does this program print?

    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);
  6. Brain teaser · Activity 6 of 7

    Brain teaser, on Linux or macOS. link.txt is a symbolic link to big.txt, which holds 1000 bytes. What does this program print?

    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);
  7. Apply · Activity 7 of 7

    Mini-task. Write main.js so that node main.js docs lists the folder docs: one line per entry, sorted by name, a folder as notes/ and a file with its size, as b.txt 4 bytes. A missing folder or a file instead of a folder prints not a folder: and the path with its code on stderr, and sets exit code 1. Make a test folder and try all three cases.

    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 folder from start to finish

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.js

Output

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: undefined
  • mkdir with recursive resolved to the first folder it had to create, demo, not to the deepest one.
  • README.md comes before notes because capital letters sort before small ones.
  • The recursive listing gives paths relative to demo, with the folders themselves included.
  • rmdir refused a folder with entries; rm with recursive removed it with everything inside.

Exercises

Exercise 1 of 2

Summarise a folder listing

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.

Hints
  1. Hint 1

    Split the list in two with filter: entries.filter((e) => e.isDirectory) and the rest.

  2. Hint 2

    Sort each part with a compare function: (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0).

  3. Hint 3

    Add up the sizes of the files only, with reduce((sum, f) => sum + f.size, 0).

Show a solution

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'));
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

// 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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Describe a real folder

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.

Hints
  1. Hint 1

    readdir(dir, {withFileTypes: true}) gives Dirent objects: entry.name and entry.isDirectory().

  2. Hint 2

    In the catch block, return [] only when error.code is ENOENT, and throw error otherwise.

  3. Hint 3

    stat(join(dir, entry.name)).size is the size; sort the records with a compare function on name.

Show a solution

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')));
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 {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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Asking a name whether it is a folder

import {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 function

Why, 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.

Removing a full folder with rmdir

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.

rm on a folder without recursive

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) data

Why, 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

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Make, list and remove folders

mkdir(dir, {recursive: true}) creates every missing folder on the way and resolves to the first one it created, or to undefined when all existed. readdir(dir) resolves to the names inside, without . and .., and without the folder part. {withFileTypes: true} gives Dirent objects with name, parentPath, isFile() and isDirectory(); {recursive: true}, there since v20.1.0, adds everything below, as paths like notes/jan.md. The docs promise no order, so sort before you print. rmdir removes only an empty folder. rm(dir, {recursive: true}) removes a folder with its contents, and force: true ignores a missing path, like rm -rf. rename moves a folder, and cp(src, dest, {recursive: true}) copies one.

stat: size, type and time

stat(path) resolves to a Stats object. size is the length in bytes, not in characters: Grüße and a newline are 8 bytes. isFile() and isDirectory() say what the path is. mtime is a Date of the last change to the content, and mtimeMs the same in milliseconds, handy for sorting newest first: (a, b) => b.mtimeMs - a.mtimeMs. A folder's size is not the size of its contents. stat follows a symbolic link to its target; lstat describes the link itself. A Dirent already knows file or folder, so you need stat only for size and times. Call it with join(dir, entry.name): the name alone is resolved against the current folder.

Just try it, and read error.code

It is tempting to ask first whether a folder exists. The fs docs advise against calling access before you open, read or write, and say the same of stat: another process may change the file between the two calls. Do the operation and handle the error instead. The codes you meet with folders: ENOENT, a part of the path does not exist; ENOTDIR, a part of the path is a file where a folder was expected, as in readdir("notes.txt"); ENOTEMPTY, rmdir on a folder that still has entries; EEXIST, mkdir without recursive on a folder that exists. Since v24.15.0, stat(path, {throwIfNoEntry: false}) resolves to undefined for a missing path instead of rejecting.

Sources

Last reviewed October 4, 2026