Skip to content
aviral gupta

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

Lesson 2 of 5 in B3 Files and paths

You will be able to

  • Read a file as a Buffer or a string, and turn its text into JSON or lines
  • Handle ENOENT, EISDIR and EACCES by error.code, and rethrow the rest
  • Compare the promise, callback and sync forms, and resolve paths against import.meta.dirname
  1. Warm-up · Activity 1 of 7

    Warm-up from B1.1: which module gives you a readFile that returns a promise, so that you can await it?

  2. 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);
  3. 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", "");
  4. Practice · Activity 4 of 7

    Your program reads a file and parses it as JSON. Match each error to the situation that causes it.

  5. 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');
  6. 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());
  7. 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.js

Output

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
  1. Hint 1

    text.split(/\r?\n/) cuts at \n and at \r\n alike.

  2. Hint 2

    filter((line) => line.trim() !== "") removes blank lines and the empty last item.

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

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

node --test
Download learnrun.js

Exercise 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
  1. Hint 1

    Wrap only the readFile call in its own try, and JSON.parse in a second one, so you know which step failed.

  2. Hint 2

    In the first catch, compare error.code with 'ENOENT' and 'EISDIR', and end with throw error; for the rest.

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

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

node --test
Download learnrun.js

Common 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 JSON

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

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

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

Bytes or text: the encoding decides

readFile(path) from node:fs/promises reads the whole file into memory. Without an encoding, it resolves to a Buffer, the raw bytes: console.log shows <Buffer 48 69 0a>, and a Buffer has no split. With 'utf8' or {encoding: 'utf8'}, you get a string. JSON.parse(text) turns JSON text into a value; broken JSON throws a SyntaxError, which is not a file error and has no error.code. For lines, text.split(/\r?\n/) cuts at every line end, also Windows ones; a final newline leaves an empty last item, so filter empty lines out. Very large files are better read with streams, later in the course.

error.code says what went wrong

A failed read rejects with an error whose code names the problem. ENOENT: no file or folder at that path. EISDIR: the path is a folder, and readFile wants a file. EACCES: the file exists, but its permissions forbid you to read it. The docs call error.code the most stable way to identify an error; the message may change between versions, so compare the code, never the message text. B1.1 fell back on ENOENT. Handle each code you expect with its own clear message, and throw everything else again, so that a real bug still stops the program.

Three forms, and where a relative path points

Every fs operation comes in three forms. The promise form, readFile from node:fs/promises, is awaited. The callback form, readFile from node:fs, calls (err, data) when done; err is null on success, so check it first. The sync form, readFileSync, returns the data and blocks: no other JavaScript runs until the file is read, and errors are thrown. A relative path like 'data.txt' is resolved against process.cwd(), the folder node was started in, not the folder of your module. For a file that ships next to main.js, build the path with join(import.meta.dirname, 'data.txt').

Sources

Last reviewed October 3, 2026