Node.js File System

Read and write files with the fs/promises API, append to logs, work with JSON files, create and list folders, check whether files exist, handle errors like ENOENT, and know when to use the synchronous and callback versions.

Lesson 4 of 43Beginner17 min read

Reading and writing files is one of the most common jobs for a Node.js program. Command-line tools process CSV exports, servers write log files, build tools read source code and write bundles, and small apps keep their data in JSON files. In Node, all of this goes through the built-in fs (file system) module.

The fs module comes in three styles. The promise API, imported from node:fs/promises, works with async and await and is the modern choice for most code. The callback API is Node's original style, still found in older code. The synchronous API, with names ending in Sync, blocks the event loop until the operation finishes; it's fine for short scripts and startup code but should be avoided inside servers.

In this lesson you'll read and write text and JSON files, append to logs, create and list folders, get information about files, handle the errors that file operations produce, and learn which style to use where. File paths are built with the path module, covered in detail in the next lesson.

Reading and writing files

JavaScript
import { readFile, writeFile, appendFile } from 'node:fs/promises';

await writeFile('notes.txt', 'First line\n');      // create or overwrite
await appendFile('notes.txt', 'Second line\n');    // add to the end
const text = await readFile('notes.txt', 'utf8');  // read as a string
  • Pass an encoding such as 'utf8' to get a string; without it, readFile returns a Buffer of raw bytes, which is right for images and other binary files.
  • writeFile replaces the whole file. Use appendFile to add to it.
  • Relative paths are resolved against the current working directory, the folder you ran node from, not the script's folder. Use import.meta.dirname to build paths next to your script.

Folders and file information

FunctionWhat it does
mkdir(path, { recursive: true })Creates a folder and any missing parents; no error if it exists
readdir(path)Lists the names inside a folder
readdir(path, { withFileTypes: true })Lists entries with isFile() and isDirectory()
stat(path)Size, creation and modification times, file or folder
rename(from, to)Renames or moves a file
copyFile(from, to)Copies a file
rm(path, { recursive: true, force: true })Deletes a file or folder

Errors and existence checks

File operations fail with an error that has a code property:

  • ENOENT: the file or folder doesn't exist.
  • EEXIST: it already exists.
  • EACCES or EPERM: permission denied.
  • EISDIR: you tried to read a folder as a file.

Rather than checking whether a file exists and then reading it (it could disappear in between), just try the operation and handle the error:

JavaScript
try {
  const data = await readFile('settings.json', 'utf8');
} catch (err) {
  if (err.code === 'ENOENT') console.log('No settings yet, using defaults');
  else throw err;
}

Node.js Basics Syntax

fs/promises

import fs from 'node:fs/promises';
const text = await fs.readFile('a.txt', 'utf8');
await fs.writeFile('b.txt', 'hello');
await fs.appendFile('log.txt', 'line\n');
const data = JSON.parse(await fs.readFile('data.json', 'utf8'));
await fs.writeFile('data.json', JSON.stringify(data, null, 2));
await fs.mkdir('out/reports', { recursive: true });
const entries = await fs.readdir('out', { withFileTypes: true });
const info = await fs.stat('a.txt');   // info.size, info.mtime
await fs.rm('out', { recursive: true, force: true });
import { readFileSync } from 'node:fs';  // sync version for scripts

Node.js Basics Summary

  • Import from node:fs/promises and use await with readFile, writeFile and appendFile.
  • Pass 'utf8' to read text; without an encoding you get a Buffer.
  • mkdir with recursive: true, readdir, stat, rename, copyFile and rm manage files and folders.
  • Errors carry codes such as ENOENT and EEXIST; try the operation and handle the error.
  • Avoid Sync functions inside servers because they block the event loop.

Node.js Basics Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A simple log file

Node.js
import { writeFile, appendFile, readFile } from 'node:fs/promises';

const file = 'app.log';
await writeFile(file, '');

async function log(level, message) {
  const stamp = '2026-03-01T09:00:00Z';
  await appendFile(file, `${stamp} [${level}] ${message}\n`);
}

await log('INFO', 'Server started');
await log('WARN', 'Disk space low');
await log('INFO', 'User signed in');

const lines = (await readFile(file, 'utf8')).trim().split('\n');
console.log(`${lines.length} entries`);
console.log(lines.filter((l) => l.includes('[WARN]')).join('\n'));
Output
3 entries
2026-03-01T09:00:00Z [WARN] Disk space low
Line-by-line explanation
  1. Line 4writeFile with an empty string creates the file, or empties it if it exists.
  2. Lines 6 to 9Each call adds one line to the end of the file. A real logger would use new Date().toISOString().
  3. Line 15readFile with utf8 returns a string, split into lines.
  4. Line 17Ordinary array methods filter the lines.
Example 2

Saving and loading JSON data

Node.js
import { readFile, writeFile } from 'node:fs/promises';

const file = 'todos.json';

async function load() {
  try {
    return JSON.parse(await readFile(file, 'utf8'));
  } catch (err) {
    if (err.code === 'ENOENT') return [];
    throw err;
  }
}

async function save(todos) {
  await writeFile(file, JSON.stringify(todos, null, 2));
}

await writeFile(file, '[]');
let todos = await load();
todos.push({ id: 1, title: 'Buy milk', done: false });
todos.push({ id: 2, title: 'Write report', done: true });
await save(todos);

todos = await load();
console.log(`${todos.length} todos, ${todos.filter((t) => !t.done).length} open`);
console.log((await readFile(file, 'utf8')).split('\n')[2]);
Output
2 todos, 1 open
    "id": 1,
Line-by-line explanation
  1. Line 7JSON.parse turns the file's text back into an array.
  2. Line 9A missing file isn't an error here: it just means no todos yet.
  3. Line 10Any other error, such as invalid permissions, is passed on.
  4. Line 15JSON.stringify with 2 writes indented, readable JSON.
  5. Line 26The third line of the saved file shows the indentation.
Example 3

Folders, listing and file information

Node.js
import { mkdir, writeFile, readdir, stat, rm } from 'node:fs/promises';
import { join } from 'node:path';

const root = 'reports-demo';
await rm(root, { recursive: true, force: true });
await mkdir(join(root, '2026', 'march'), { recursive: true });
await writeFile(join(root, 'summary.txt'), 'Total: 1204 orders');
await writeFile(join(root, 'notes.md'), '# Notes');

const entries = await readdir(root, { withFileTypes: true });
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
  console.log(`${e.isDirectory() ? 'folder' : 'file  '} ${e.name}`);
}

const info = await stat(join(root, 'summary.txt'));
console.log(`summary.txt is ${info.size} bytes, file: ${info.isFile()}`);

try {
  await stat(join(root, 'missing.txt'));
} catch (err) {
  console.log(`Error code: ${err.code}`);
}
Output
folder 2026
file   notes.md
file   summary.txt
summary.txt is 18 bytes, file: true
Error code: ENOENT
Line-by-line explanation
  1. Line 5force: true means no error if the folder doesn't exist yet.
  2. Line 6recursive: true creates 2026 and march in one call.
  3. Line 10withFileTypes returns entries that know whether they're files or folders.
  4. Line 11The names are sorted, because readdir doesn't promise any order.
  5. Line 15stat reports the size in bytes, among other details.
  6. Lines 19 to 21A missing file produces the ENOENT error code.

Common Node.js Basics Mistakes

Forgetting the encoding

Don't
const text = await readFile('notes.txt');
console.log(text.toUpperCase());          // TypeError: text.toUpperCase is not a function (it's a Buffer)
Do
const text = await readFile('notes.txt', 'utf8');
console.log(text.toUpperCase());

Forgetting await

Don't
const data = readFile('data.json', 'utf8');
JSON.parse(data);                         // data is a Promise, not text
Do
const data = await readFile('data.json', 'utf8');
JSON.parse(data);

Sync file reads inside a request handler

Don't
app.get('/report', (req, res) => {
  res.send(readFileSync('big-report.html', 'utf8'));   // blocks every other request
});
Do
app.get('/report', async (req, res) => {
  res.send(await readFile('big-report.html', 'utf8'));
});

Node.js Basics in Real Projects

File handling in Node projects

Build tools such as Vite and webpack read thousands of source files and write bundles with fs. Static site generators read Markdown and write HTML. Servers write logs and uploaded files, CLI tools process CSV and JSON exports, and configuration is often read once at startup from a JSON file. For very large files, Node's streams process data piece by piece instead of loading it all into memory.

Node.js Basics FAQ

How do I check if a file exists in Node.js?

In most cases, don't check first: try to read or open it and catch the ENOENT error, because the file could be created or deleted between a check and the real operation. When you only need a yes or no, for example in a startup script, existsSync(path) from node:fs returns true or false. The old callback fs.exists is deprecated.

How do I read a file line by line in Node.js?

Open it as a stream and pass it to readline: const rl = createInterface({ input: createReadStream('log.txt'), crlfDelay: Infinity }), then loop with for await (const line of rl). This keeps memory use low even for multi-gigabyte files. For small files, (await readFile(path, 'utf8')).split('\n') is simpler.

What is the difference between readFile and createReadStream?

readFile loads the whole file into memory and gives it to you in one piece, which is easiest for small and medium files. createReadStream delivers the file in chunks (64 KB by default) as they're read, so memory use stays flat whatever the file size. Use streams for large files, uploads and anything you're piping on to another destination.

Why does writeFile throw ENOENT when I'm creating a new file?

writeFile creates the file but not the folders above it, so the error means a folder in the path doesn't exist. Create it first with await mkdir(path.dirname(file), { recursive: true }), which does nothing if the folder is already there. Also check that the relative path is relative to where you ran node, not to the script.

How do I write to a file safely so a crash doesn't leave it half-written?

Write the new content to a temporary file in the same folder, then rename it over the original. On Linux and macOS, a rename within one file system replaces the file in a single step, so readers see either the old version or the new one, never a partial file. Many libraries that save JSON settings use exactly this pattern.

Node.js Basics Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line to read the file as text.

const text = await readFile('poem.txt', '');
Multiple choiceeasy

Which import gives you promise-based file functions?

Multiple choicemedium

What does the error code ENOENT mean?

Predict the outputmedium

What does this print?

await writeFile('a.txt', 'one');
await writeFile('a.txt', 'two');
console.log(await readFile('a.txt', 'utf8'));
Multiple choicehard

Why avoid readFileSync in a web server's request handler?

Node.js Basics Quiz

5 questions to check what you learned.

  1. Which function adds text to the end of a file?

    • appendFile
    • writeFile
    • addFile
    • readFile
  2. What does mkdir with { recursive: true } do?

    • Deletes folders recursively
    • Lists folders
    • Creates any missing parent folders and doesn't fail if the folder exists
    • Copies folders
  3. What does readFile return when no encoding is given?

    • A string
    • An array of lines
    • null
    • A Buffer of bytes
  4. Relative paths in fs calls are resolved against which folder?

    • The current working directory where node was started
    • The script's folder
    • The home folder
    • node_modules
  5. Which is the safer way to read a file that might not exist?

    • Check with existsSync, then read
    • Use writeFile first
    • Use stat in a loop
    • Try to read it and handle the ENOENT error