The fs Module
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.
Learn The fs Module in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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.
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.
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.
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.
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.
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)
Frequently Asked Questions
🎉 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
- ✅ Next lesson: Paths & OS — build cross-platform paths and read system info
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