The fs Module

Reviewed & published by Brayan K

The fs (file system) module is Node's built-in tool for reading, writing, updating, and deleting files and directories on disk, available in synchronous, callback, and promise-based forms.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll know how to read a file into a string, write and append data, work with directories, check whether a path exists, and delete files — and you'll understand when to reach for the fast non-blocking promise API instead of the simpler synchronous one.

What You'll Learn in This Lesson

1️⃣ Reading Files

Reading is the most common file operation. There are two main styles. fs.readFileSync is synchronous: it pauses your program until the file is fully read, then hands back the contents. It's easy to reason about, which is why it's fine in short scripts — but it blocks the event loop, so it's a poor fit for servers.

The modern alternative is the promise-based API: import { readFile } from 'node:fs/promises' and then await readFile(...). It does the same job without freezing everything else. In both cases, pass 'utf8' as the second argument so you get a readable string back instead of a raw Buffer of bytes.

// The fs module is built into Node — no install needed.
// This file is an ES module (name it .mjs, or set "type": "module" in
// package.json). That is what allows the top-level await further down.
import fs from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises';

// In a CommonJS file you would write this instead:
//   const fs = require('node:fs');
// and top-level await would NOT be available. A file is one or the other —
// mixing require and import in a single file is an error, not a style choice.

// Create the file this example reads, so the block stands on its own:
await writeFile('notes.txt', 'Buy milk', 'utf8');

// Synchronous read: simple, but it BLOCKS the event loop until done.
// Pass 'utf8' so you get a STRING back. Without it you get a Buffer.
const text = fs.readFileSync('notes.txt', 'utf8');
console.log(text);                 // -> Buy milk

// The modern, non-blocking way: the promise-based API + await.
const data = await readFile('notes.txt', 'utf8');
console.log(`Read ${data.length} characters`);   // -> Read 8 characters

2️⃣ Writing & Appending

Writing comes in two flavours. fs.writeFileSync(path, data) creates the file if it doesn't exist and overwrites it completely if it does — so use it carefully. The promise version, writeFile from node:fs/promises, does the same thing without blocking.

When you want to add to a file rather than replace it — think log files — reach for fs.appendFile (or appendFileSync). It writes your text onto the end and creates the file first if it isn't there yet.

// An ES module again, for the top-level await below.
// (CommonJS equivalent of the first line: const fs = require('node:fs');)
import fs from 'node:fs';
import { writeFile } from 'node:fs/promises';

// writeFileSync CREATES the file (or OVERWRITES it if it already exists).
fs.writeFileSync('log.txt', 'First line\n', 'utf8');

// The promise version does the same thing without blocking.
await writeFile('greeting.txt', 'Hello!', 'utf8');

// appendFile ADDS to the end instead of overwriting — handy for logs.
fs.appendFileSync('log.txt', 'Second line\n', 'utf8');

console.log(fs.readFileSync('log.txt', 'utf8'));

3️⃣ Directories & Existence

Files live in directories, and fs handles those too. fs.mkdir creates a folder — pass { recursive: true } to build any missing parent folders and to avoid an error if it already exists. fs.readdir lists what's inside a folder as an array of names.

To check whether something is there, fs.existsSync(path) returns a simple true/false. For richer information, fs.stat returns metadata (size, and helpers like isDirectory()). Finally, fs.unlink deletes a file.

const fs = require('node:fs');

// Create a folder. { recursive: true } makes parent folders too
// and won't throw if the folder already exists.
fs.mkdirSync('data/reports', { recursive: true });

// List everything inside a directory (returns an array of names).
const files = fs.readdirSync('data');
console.log(files);                       // -> [ 'reports' ]

// existsSync answers a yes/no question before you act.
console.log(fs.existsSync('data/reports'));   // -> true
console.log(fs.existsSync('nope.txt'));       // -> false

// stat gives you metadata: size, is it a file or a directory, etc.
const info = fs.statSync('data');
console.log(`Is directory? ${info.isDirectory()}`);   // -> Is directory? true

// unlink DELETES a file (it throws if the file isn't there).
fs.writeFileSync('temp.txt', 'bye');
fs.unlinkSync('temp.txt');
console.log(fs.existsSync('temp.txt'));   // -> false

Your turn. The program below works once you fill in the two blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output.

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

async function main() {
  // 🎯 YOUR TURN — replace each ___ then run it.

  // 1) Write the text "Learning Node" into a file called note.txt
  await writeFile('note.txt', ___, 'utf8');   // 👉 text in 'single quotes'

  // 2) Read it back as a STRING (don't forget the encoding!)
  const text = await readFile('note.txt', ___);   // 👉 the encoding: 'utf8'

  console.log(`File says: ${text}`);
}

main();

// ✅ Expected output:
//    File says: Learning Node

Mini-Challenge: A Tiny Journal

No blanks this time — just a brief and an outline to keep you on track. Write it yourself, run it, and check your output against the example in the comments. This ties together writing, appending, and reading in one short program.

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

async function main() {
  // 🎯 MINI-CHALLENGE: a tiny journal
  // 1. Write the line "Day 1\n" into journal.txt (overwrites/creates it).
  // 2. Append the line "Day 2\n" to the same file.
  // 3. Read the whole file back as a 'utf8' string and print it.
  // (Use writeFile, then appendFile, then readFile — all with await.)
  //
  // ✅ Example output:
  //    Day 1
  //    Day 2

  // your code here
}

main();

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — The fs Module

Taskfs Syntax
Read (sync)fs.readFileSync(p, 'utf8')
Read (promise)await readFile(p, 'utf8')
Writefs.writeFileSync(p, data)
Appendfs.appendFileSync(p, data)
List a directoryfs.readdirSync(d)
Does it exist?fs.existsSync(p)
Delete a filefs.unlinkSync(p)

🎉 Lesson Complete!

Practice quiz

What do you get back from fs.readFileSync('f.txt') with no encoding argument?

  • A string
  • A Buffer of raw bytes
  • An array of lines
  • null

Answer: A Buffer of raw bytes. Without an encoding, fs returns the file's raw bytes as a Buffer; pass 'utf8' to get a string.

How do you read a file as a string?

  • fs.readFileSync('f.txt', 'utf8')
  • fs.readFileSync('f.txt', 'string')
  • fs.readFileSync('f.txt').string()
  • fs.readString('f.txt')

Answer: fs.readFileSync('f.txt', 'utf8'). Passing 'utf8' as the second argument returns readable text instead of a Buffer.

Why is fs.readFileSync a poor fit inside a busy server?

  • It corrupts the file
  • It cannot read large files
  • It blocks the event loop until the disk finishes, so other requests wait
  • It always returns a Buffer

Answer: It blocks the event loop until the disk finishes, so other requests wait. The synchronous call freezes the single event loop, stalling every other request.

Which import gives you the non-blocking, promise-based file API?

  • require('node:fs').promises only
  • import fs from 'node:async-fs'
  • require('fs-promises')
  • import { readFile } from 'node:fs/promises'

Answer: import { readFile } from 'node:fs/promises'. node:fs/promises provides promise-based functions you use with await.

What does fs.writeFileSync do if the target file already exists?

  • Appends to the end
  • Overwrites it completely
  • Throws an error
  • Creates a numbered copy

Answer: Overwrites it completely. writeFileSync creates the file or overwrites it entirely if it already exists.

Which function adds to the end of a file instead of replacing it?

  • fs.appendFileSync
  • fs.writeFileSync
  • fs.mkdirSync
  • fs.updateSync

Answer: fs.appendFileSync. appendFile/appendFileSync writes onto the end of the file, creating it first if needed.

What does { recursive: true } do in fs.mkdirSync('data/reports', { recursive: true })?

  • Deletes existing folders first
  • Lists the directory contents
  • Creates missing parent folders and doesn't throw if it already exists
  • Makes the folder read-only

Answer: Creates missing parent folders and doesn't throw if it already exists. recursive: true builds any missing parent folders and avoids an error when the folder exists.

Why is calling fs.existsSync before reading often discouraged?

  • It is deprecated and removed
  • It introduces a race condition and adds an extra blocking call
  • It always returns true
  • It deletes the file

Answer: It introduces a race condition and adds an extra blocking call. The file can change between the check and the action; better to attempt the operation and handle errors.

What does an 'ENOENT: no such file or directory' error usually mean?

  • The file is locked by another process
  • You lack permission to read it
  • The disk is full
  • The path is wrong, often relative to where you ran the process

Answer: The path is wrong, often relative to where you ran the process. ENOENT means Node found nothing at that path; build absolute paths with __dirname and path.join.

Which function deletes a file?

  • fs.unlinkSync
  • fs.rmdirSync
  • fs.removeSync
  • fs.deleteSync

Answer: fs.unlinkSync. fs.unlink/unlinkSync deletes a file and throws if the file isn't there.

Continue this course

Frequently asked questions

What is the difference between fs.readFileSync and the promise-based readFile?

fs.readFileSync is synchronous: it stops everything and waits for the disk before returning the data, which is fine for a one-off startup script but freezes the whole event loop in a server. The promise-based readFile (from 'node:fs/promises') is asynchronous: you await it, Node keeps handling other work while the disk is busy, and you get the data when it's ready. Prefer the promise API in anything that serves users; the sync API is mainly for short scripts and config loading at boot.

Why did fs.readFile give me a Buffer instead of a string?

If you don't pass an encoding, fs returns the file's raw bytes as a Buffer object so it can handle any file, including images and binaries. To get readable text, pass 'utf8' (for example fs.readFileSync('file.txt', 'utf8')) or call .toString('utf8') on the Buffer yourself. The same rule applies to readdir and the promise versions.

What is node:fs/promises and why use it?

node:fs/promises is the built-in promise-based version of the fs module. Instead of nesting callbacks, you import functions like readFile and writeFile and use async/await, which reads top-to-bottom and integrates cleanly with try/catch for error handling. It does the same work as the callback API but with far nicer, more readable code, so it's the recommended choice for new projects.

Should I check fs.existsSync before reading or writing a file?

Usually no. existsSync introduces a race condition — the file can be created or deleted between your check and your action — and adds an extra blocking call. The idiomatic pattern is to just try the operation and handle the error (for example catch an ENOENT when a file is missing). existsSync is fine for simple scripts, but for robust code, attempt the read/write and react to what actually happens.