Node.js File System Explained: Sync vs Async Methods & Best Practices
(Node Series Part 5)
In this section, we will cover the FS module.
FS Module (File System)
The Node.js `fs` (File System) module handles file and directory operations.
🍀Provides both synchronous and asynchronous methods.
🍀Synchronous methods pause execution until completion.
🍀Asynchronous methods use callbacks/promises for parallel execution.
🍀Supports all file types: text, JSON, binary data (images, etc.)
🍀Common operations: read, write, update, delete, rename, copy files.
🍀Built into Node.js (no installation required)
🍀Essential for server-side file management tasks.
It provides an API for interacting with the file system
Rule to Remember
Be aware, in Unix-based systems we use a backward slash(/) but in Windows we use a forward slash(\), for this reason, never construct a path manually. Always use the path module.
Always use the Path Module to construct a path.
If you are not using any bundler:
✨`index.js` won’t get auto-resolved.
✨file `extension` won’t get resolved
Absolute and Relative Path
Absolute Path: An absolute path points to a file or directory starting from the root of the file system (or drive).
It’s a complete address — it tells the OS exactly where to find the file, no matter where you are.
Linux/Mac
`/home/shubham/projects/myapp/src/index.js
/var/log/system.log`
Win:
`C:\Users\shubham\Documents\notes.txt
D:\Projects\myapp\src\index.js`Relative Path: A relative path locates a file/directory relative to the current working directory (CWD) or another base path.
Examples of relative paths:
Assume your current working directory is
/home/shubham/projects/myapp🐾./src/index.js ->/home/shubham/projects/myapp/src/index.js
🐾../README.md -> one directory up: /home/rahul/projects/README.md
🐾./config/settings.json ->. means “current directory”, so /home/shubham/projects/myapp/config/settings.json.
The `fs` module accepts **both relative and absolute paths. But relative paths are resolved against the current working directory** (process.cwd()), not the script’s file location.
🔥 What does that mean?
If you give an absolute path, fs uses it directly. No ambiguity is there.
import fs from "fs";
const absPath = "/Users/shubham/myapp/data.txt";
const data = fs.readFileSync(absPath, "utf-8");
console.log(data);If you give a relative path, Node resolves it relative to where you ran the process from (the working directory).
Now, here is an example of a relative path. I have added a folder a/b with b.js and log.txt in folder b
Inside my file b.js, I am simply trying to read data
import fs from "fs";
fs.readFile("./log.txt", "utf-8",function (err,data) {
console.log(data)
});Now in my terminal, I go to the folder b and then run b.js
node b.jsNow, if I go back to my root folder and try to run it
due to a relative path, it causes an issue since I am executing it from another place.
Now solution for this is simple: construct a path using the path module
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.join(__dirname, "./log.txt");
fs.readFile(filePath, "utf-8",function (err,data) {
console.log(data)
});⚠️Common gotcha:
People think relative paths in
fsare relative to the JS file’s directory — but they’re not.They’re always relative to
process.cwd()(your terminal’s current directory where you run node ...).
🚨 Key takeaway:
- ✅ Absolute paths: always safe, no ambiguity.
- ⚠️ Relative paths: relative to process.cwd(), not the script file’s directory.
Encoding
Encoding is the way we represent text as bytes (binary data) for storage or transmission.
Different encodings interpret byte sequences differently.
Hello 👋
This string includes:
- Basic ASCII: Hello
- Unicode character: 👋 (waving hand emoji)Options in Node
const text = "Hello 👋";
const bufUtf8 = Buffer.from(text, 'utf-8');
const bufAscii = Buffer.from(text, 'ascii');
const bufBase64 = bufUtf8.toString('base64');✅ UTF-8
console.log(bufUtf8);
// Output: <Buffer 48 65 6c 6c 6f 20 f0 9f 91 8b>
- Hello is 48 65 6c 6c 6f (standard ASCII bytes)
- 👋 is f0 9f 91 8b (multi-byte UTF-8 character)✅ ASCII
console.log(bufAscii);
// Output: <Buffer 48 65 6c 6c 6f 20 3f>
- Hello is the same
- 👋 becomes ? → ASCII can’t represent emoji,
so it **replaces with ?** (code 63)
❌ ASCII is limited to **0–127** characters only (A-Z, a-z, 0-9, symbols)✅ Base64
console.log(bufBase64);
// Output: SGVsbG8g8J+Riwo=
- This is **binary data encoded as text** — not human-readable
- Used in image uploads, JWTs, email, etc.
- Can be decoded back to original✅ null(default): Get raw Buffer (‘utf-8’ default method)
const raw = Buffer.from(text); // same as 'utf-8'
console.log(raw); // <Buffer 48 65 6c 6c 6f 20 f0 9f 91 8b>Common Operations in ‘fs’
In a node, it deals with OS, not the user OS, but where the node code is deployed.
We import the “fs” module where we want to perform fs operations
import fs from "fs";
// prefer below onne
import fs from "node:fs";
// node: tells it is imported from node js not third party
// just to differentiate🍀Read File
fs.readFile(path,options[encoding],callback(err,data){})
// path - path to file
// options - usually we pass encoding
// callback fucntion after reading where data is content of file.Example: I will try to read my package.json. Here is an example in my index.js
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.join(__dirname, "package.json");
fs.readFile(filePath, "utf-8", function(err, data) {
if (err) {
console.log(err);
return;
}
console.log(data);
});the data retuned is a string , u have to parse it then it will work.
Here is an example
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.join(__dirname, "package.json");
fs.readFile(filePath, "utf-8", function(err, data) {
if (err) {
console.log(err);
return;
}
const obj=JSON.parse(data)
console.log(obj.version);
});🍀Write File
This method can be used to create a new file or overwrite a file.
fs.writeFile(path,dataToUpdate,callback(err))
// path - file path
// dataToUpdate - data you want to update
// callback - callback function.
// if path is not there it create oneHere, I have updated the version in my package.json
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.join(__dirname, "package.json");
fs.readFile(filePath, "utf-8", function(err, data) {
if (err) {
console.log(err);
return;
}
const obj=JSON.parse(data)
console.log(obj.version);
obj.version="1.0.2"
fs.writeFile(filePath,JSON.stringify(obj),function (err) {
if(err){
console.log(err);
}
console.log("version updated");
})
});This write method can also be used to create a new file
🍀Append File
Adding some data at the end of the file.
fs.appendFile(path, dataToAppend,callback(err))
// path- file where u want to append data
// dataToAppend - data u want to add at the end of the file
// callback - to handle error or log Example: I have appended the data inside my notes.txt file
This is my current notes.txt file
In my index.js
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.join(__dirname, "notes.txt");
fs.appendFile(filePath, "To Our Beautifull Death's", function (err) {
if (err) {
console.log(err);
}
console.log("notes appended");
})🍀Create a new Directory
fs.mkdir(path, {options}, callback(err))
// path - Directory path to create
// options (Optional) -recursive and mode passed inside and object
// callback - to handle error or logoptions :
Get Shubham Gupta’s stories in your inbox
Join Medium for free to get updates from this writer.
🐾recursive (Boolean): Create parent directories if they don’t exist (default: false)
🐾mode (Integer): Permission bits (e.g., 0o755) (default: 0o777)
Here is an implementation example. I will try to create a folder named outer
import fs from "fs";
fs.mkdir('./outer', function (err) {
if (err) {
console.log(err);
}else{
console.log("directory added");
}
})I created the folder outer now I will add two folders named inner and another one innderdeep where both do not exist.
outer-innner-innerdeep
import fs from "fs";
fs.mkdir('./outer/inner/innerdeep', { recursive: true }, function(err) {
if (err) {
console.log("Error:", err);
} else {
console.log("All directories created successfully!");
}
});Before:
After:
🍀Check if a file or directory exists
fs.existsSync(path)
// path - file or dir path
// returns true or falseHere is an example
import fs from 'fs';
const path = './outer';
if (fs.existsSync(path)) {
console.log('✅ Directory exists!');
} else {
console.log('❌ Directory not found.');
}🍀 Read the contents of a directory
fs.readdir(path, {options}, callback(err, files))
// path - path to dir
// options - inside and object withFileTypes and encoding
// callback - has err and files inside directory,Options:
🐾encoding (String): File name encoding (e.g., ‘utf8’). Default: ‘utf8’.
🐾withFileTypes (Boolean): Return fs.Dirent objects instead of strings. Default: false.
Here is an example below, I will read my outer directory
import fs from "fs";
fs.readdir('./outer', (err, files) => {
if (err) return err;
console.log(files);
});Now if I want to get the details of the file we use writeFiletype
fs.readdir('./outer', { withFileTypes: true }, (err, files) => {
if (err) return err;
console.log(files)
// fs.dirent type objects are returned it has method isFile
// returns 1 for file and 2 for directory
files.forEach(dirent => {
console.log(dirent.name, dirent.isFile() ? '[File]' : '[Directory]');
});
});🍀Get information about a file/Directory
fs.stat(path, callback(err, stats))
// path (String | Buffer | URL): File/directory path to inspect.
// callback (Function):
//(err, stats) where stats is an fs.Stats object containing file metadata.import fs from "fs";
fs.stat('./outer', (err, stats) => {
if (err) throw err;
console.log(stats);
});Here you can see metadata of the file, below table explains the detail
Here is a more practical example
import fs from "fs";
fs.stat('./outer', (err, stats) => {
if (err) throw err;
console.log(`
📄 ${stats.isFile() ? 'File' : 'Folder'}
📏 Size: ${stats.size} bytes
🕒 Modified: ${stats.mtime.toLocaleString()}
🔒 Permissions: 0${(stats.mode & 0o777).toString(8)}
${stats.isFile() ? '✅ Executable' : 'Is A Directory'}
`)
});🍀Delete a File
fs.unlink(path, callback(err))
// path (string | Buffer | URL): Path to the file to delete.
// callback (Function): Called after the operation completes.
// err (Error | null): Contains an error if deletion fails.Example
import fs from "fs";
fs.unlink('./outer/outer.txt', (err) => {
if (err) {
console.error('Error deleting file:', err);
return;
}
console.log('File was deleted');
});🍀Rename/Move a File
fs.rename(oldPath, newPath, callback);
// oldPath: Current file path (string, Buffer, or URL).
// newPath: New file path (rename or move).
// callback: (err) => {} (error-first callback).Here is an example for renaming
import fs from "fs";
fs.rename('./outer/test.txt', 'newname.txt', (err) => {
if (err) throw err;
console.log('File renamed!');
});Here is an example of moving files
import fs from "fs";
fs.rename('./outer/test.txt', './outer2/test.txt', (err) => {
if (err) throw err;
console.log('File Moved!');
});🍀Copy a File
fs.copyFile(src, dest[, flags], callback(err));
// src: Source file path.
// dest: Destination file path.
// flags (optional):fs.constants.COPYFILE_EXCL ,
// fs.constants.COPYFILE_FICLONE
// callback: (err) => {}.//Flags
fs.constants.COPYFILE_EXCL (fails if destination exists).
fs.constants.COPYFILE_FICLONE (copy using copy-on-write).Here is an example
fs.copyFile('./outer/source.txt', './outer2/destination.txt', (err) => {
if (err) throw err;
console.log('File copied!');
});FLags
fs.constants.COPYFILE_EXCL
This prevents overwriting if the file already exists; instead, it throws an error. However, if the file does not exist, it creates the file and copies the source data.
import fs from 'fs';
// Fails if destination already exists
fs.copyFile(
'./outer/source.txt',
'./outer2/destination.txt',
fs.constants.COPYFILE_EXCL, // Flag to prevent overwrite
(err) => {
if (err) {
console.error('Error:', err.message);
if (err.code === 'EEXIST') {
console.log('File already exists!');
}
return;
}
console.log('File copied (no overwrite)!');
}
);It shows an error as the file is already there
fs.constants.COPYFILE_FICLONE
This flag enables copy-on-write (COW) optimization when supported by the filesystem (like btrfs, XFS, or APFS on macOS).
How It Works
Normal Copy (without flag):
- Reads entire file → Writes new copy (double disk I/O)
- Uses 2x storage temporarily during copy
With COPYFILE_FICLONE:
- Creates a metadata reference to the original file
- Only writes new data when either file is modified (“lazy copy”)
- Uses near-zero additional disk space initially
3 Ways to do I/O in Node
🍀Read Sync(Blocking)
It blocks further operation until it is finished. Each method has its sync method.
import { readFileSync } from 'fs';
// Blocking operation
try {
const data = readFileSync('file.txt', 'utf8');
console.log('Sync:', data);
} catch (err) {
console.error(err);
}Best for CLI tools and Startup/config loading
🍀Read Async /Callback Style (Original)
All the above example uses this method.
import fs from 'fs';
// Asynchronous with callbacks
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('Callback:', data);
});🍀Promise Style (Modern)
import { readFile } from 'node:fs/promises';
// Using async/await
async function readFilePromise() {
try {
const data = await readFile('file.txt', 'utf8');
console.log('Promise:', data);
} catch (err) {
console.error(err);
}
}
readFilePromise();Best for Clean error handling and Modern codebases
Always prefer promise-based APIs, which are cleaner.
If not available, try to promisify it using util.promisify.
Error Codes
Some common Error codes in fs operation
If you’d like to be in the know, subscribe, clap, like and share. Cheers!







