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
- How to read a file as text with readFileSync and the promise API
- Why passing 'utf8' gives you a string instead of a Buffer
- How to write and append data with writeFile and appendFile
- How to create and list directories with mkdir and readdir
- How to check existence and inspect files with existsSync and stat
- When to use blocking sync fs vs the non-blocking node:fs/promises API
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 characters2️⃣ 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')); // -> falseYour 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 NodeMini-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)
- "ENOENT: no such file or directory" — Node looked where you pointed and found nothing. The path is usually relative to where you ran the process, not where the file lives. Build an absolute path with __dirname (or import.meta.dirname) plus path.join, and double-check the filename.
- Got a <Buffer 42 75 79 ...> instead of text? — You forgot the encoding. fs.readFileSync('f.txt') returns raw bytes; add 'utf8' as the second argument to get a string.
- Your server feels frozen under load — A ...Sync call blocks the single event loop, so every other request waits. In servers, switch to the node:fs/promises API with await.
- "EACCES: permission denied" — The file or folder exists, but your process isn't allowed to read or write it. Check ownership and permissions, or write somewhere your user controls (like a temp or project folder).
- "Promise { <pending> }" printed instead of data — You used the promise API but forgot await (and an enclosing async function). Without awaiting, you're logging the promise itself, not its result.
Pro Tips
- 💡 Default to the promise API: node:fs/promises with await reads cleanly and won't block your event loop. Keep the ...Sync calls for tiny startup scripts.
- 💡 Always build paths with the path module: combine __dirname and path.join('data', 'file.txt') so your code works on every operating system.
- 💡 Use the node: prefix (require('node:fs')) to make it unmistakable you mean the built-in module, not a package of the same name.
📋 Quick Reference — The fs Module
| Task | fs Syntax |
|---|---|
| Read (sync) | fs.readFileSync(p, 'utf8') |
| Read (promise) | await readFile(p, 'utf8') |
| Write | fs.writeFileSync(p, data) |
| Append | fs.appendFileSync(p, data) |
| List a directory | fs.readdirSync(d) |
| Does it exist? | fs.existsSync(p) |
| Delete a file | fs.unlinkSync(p) |
🎉 Lesson Complete!
- ✅ fs is Node's built-in module for reading, writing, and deleting files and folders
- ✅ readFileSync(p, 'utf8') returns a string; without 'utf8' you get a Buffer
- ✅ The node:fs/promises API with await is the non-blocking choice for servers
- ✅ writeFile overwrites; appendFile adds to the end
- ✅ mkdir, readdir, existsSync, stat, and unlink handle directories, checks, and deletes
- ✅ Most "ENOENT" errors are really a wrong path — build paths with __dirname
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
- Previous: Core Globals (process, __dirname)
- Next: Paths & OS — Build cross-platform paths with path and inspect the system with os
- Quick reference: Node.js cheat sheet › Core Modules (fs, path, http)
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.