Sitemap
Shubham Gupta

Shubham Gupta

React Developer

Node.js File System Explained: Sync vs Async Methods & Best Practices

(Node Series Part 5)

11 min readJul 12, 2025

--

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.js
Press enter or click to view image in full size

Now, if I go back to my root folder and try to run it

Press enter or click to view image in full size

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)
});
Press enter or click to view image in full size

⚠️Common gotcha:

People think relative paths in fs are 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);
});
Press enter or click to view image in full size

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 one

Here, 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");

})
});
Press enter or click to view image in full size

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

Press enter or click to view image in full size

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");

})
Press enter or click to view image in full size

🍀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 log

options :

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");
}

})
Press enter or click to view image in full size

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:

Press enter or click to view image in full size

After:

Press enter or click to view image in full size

🍀Check if a file or directory exists

fs.existsSync(path)

// path - file or dir path

// returns true or false

Here is an example

import fs from 'fs';

const path = './outer';

if (fs.existsSync(path)) {
console.log('✅ Directory exists!');
} else {
console.log('❌ Directory not found.');
}
Press enter or click to view image in full size

🍀 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);
});
Press enter or click to view image in full size

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]');
});
});
Press enter or click to view image in full size

🍀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);
});
Press enter or click to view image in full size

Here you can see metadata of the file, below table explains the detail

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

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');
});
Press enter or click to view image in full size

🍀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!');
});
Press enter or click to view image in full size

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!');
});
Press enter or click to view image in full size

🍀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!');
});
Press enter or click to view image in full size

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

Press enter or click to view image in full size

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):

  1. Reads entire file → Writes new copy (double disk I/O)
  2. Uses 2x storage temporarily during copy

With COPYFILE_FICLONE:

  1. Creates a metadata reference to the original file
  2. Only writes new data when either file is modified (“lazy copy”)
  3. 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

Press enter or click to view image in full size

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

Press enter or click to view image in full size

If you’d like to be in the know, subscribe, clap, like and share. Cheers!

--

--