The fs Module: Reading and Writing Files
Objectives
By the end of this lesson, you should be able to:
- Read and write files synchronously and asynchronously with
fs - Choose between the sync, callback, and promise-based versions of
fs - Create directories and list their contents
💡 Why this matters: Reading configuration, writing logs, serving uploaded files, all of it goes through
fs. It’s one of Node’s oldest and most-used built-in modules, and Express applications lean on it constantly.
⚠️ A note on verification: every snippet and output in this lesson was actually run with Node.js.
Synchronous File Operations
const fs = require('fs');
fs.writeFileSync('notes.txt', 'First line of notes.\n');
fs.appendFileSync('notes.txt', 'Second line, appended.\n');
const contents = fs.readFileSync('notes.txt', 'utf8');
console.log(contents);
console.log('exists:', fs.existsSync('notes.txt'));
fs.unlinkSync('notes.txt');
console.log('exists after delete:', fs.existsSync('notes.txt'));
First line of notes.
Second line, appended.
exists: true
exists after delete: false
writeFileSync creates a file (or overwrites it if it already exists), appendFileSync adds to the end of an existing file instead. The 'utf8' argument to readFileSync tells it to return a string, without it, readFileSync returns a raw Buffer of bytes. existsSync checks whether a path exists at all, unlinkSync deletes a file.
Every function ending in Sync blocks the entire program until the operation finishes, fine for small scripts and one-off tooling, but Lesson 3 (process) aside, blocking the whole program is exactly what the event loop (Module 3) is designed to avoid in a real server.
Asynchronous File Operations (Callbacks)
const fs = require('fs');
fs.writeFile('async-notes.txt', 'Written asynchronously.\n', (err) => {
if (err) throw err;
fs.readFile('async-notes.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('read:', data);
fs.unlink('async-notes.txt', (err) => {
if (err) throw err;
console.log('deleted');
});
});
});
console.log('this logs first, before the file finishes writing');
this logs first, before the file finishes writing
read: Written asynchronously.
deleted
The non-Sync versions take a callback as their last argument, and don’t block, the line after fs.writeFile runs immediately, before the write even completes. Notice the nesting: reading only makes sense after writing finishes, so readFile is called inside writeFile’s callback, this pattern is called “callback hell” once it gets a few levels deep, one of the reasons Promises (Module 2) and async/await exist.
The Promise-Based fs API
const fs = require('fs/promises');
async function run() {
await fs.writeFile('promise-notes.txt', 'Written with promises.\n');
const data = await fs.readFile('promise-notes.txt', 'utf8');
console.log('read:', data);
await fs.unlink('promise-notes.txt');
console.log('deleted');
}
run();
read: Written with promises.
deleted
require('fs/promises') gives a version of every fs function that returns a Promise instead of taking a callback, combined with async/await (Module 2), this reads almost identically to the synchronous version, but without blocking the event loop. This is the version most real Node.js and Express code reaches for today.
Working with Directories
const fs = require('fs');
fs.mkdirSync('demo-dir');
fs.writeFileSync('demo-dir/a.txt', 'a');
fs.writeFileSync('demo-dir/b.txt', 'b');
console.log(fs.readdirSync('demo-dir'));
const stats = fs.statSync('demo-dir/a.txt');
console.log('is file:', stats.isFile());
console.log('is directory:', stats.isDirectory());
fs.unlinkSync('demo-dir/a.txt');
fs.unlinkSync('demo-dir/b.txt');
fs.rmdirSync('demo-dir');
[ 'a.txt', 'b.txt' ]
is file: true
is directory: false
mkdirSync creates a directory, readdirSync lists its contents, and statSync returns metadata about a path, including isFile()/isDirectory() to tell what kind of thing it is. A directory has to be emptied (every file inside removed) before rmdirSync can remove it.
Try It
- Write a script that creates a file, appends two more lines to it one at a time, then reads and prints the full contents, using the synchronous API.
- Rewrite the same script using the callback-based API, nesting each step inside the previous callback.
- Rewrite it again using
fs/promisesandasync/await, and compare how much easier it reads compared to the callback version. - Create a directory containing three files, list its contents with
readdirSync, then usestatSyncon one file to confirmisFile()returnstrue.
Recap
fs’sSyncfunctions block until finished, callback-based functions don’t block but nest awkwardly,fs/promisescombines the non-blocking behavior with cleanasync/awaitsyntax.writeFile/appendFile/readFile/unlinkcover the core file operations,mkdir/readdir/stat/rmdircover directories.statSync(or its async equivalents) reveals whether a path is a file or a directory.
Next lesson: the path module, building file paths correctly.