The fs module is a built-in Node.js module used to work with the file system of your computer or server. It allows a Node.js application to read files, write files, append content, rename files, delete files, inspect directories, and perform many other file-related operations. Since many real applications store logs, read templates, handle uploads, export reports, or load configuration files, the file system module is one of the most practical modules to learn early in Node.js.
The file system module provides both asynchronous and synchronous methods. Asynchronous methods are generally preferred in production because they do not block the main event loop while waiting for disk operations. Synchronous methods are sometimes useful in simple scripts, quick setup tasks, or small utilities where blocking behavior is acceptable and code simplicity matters more than concurrency.
The file system module is built into Node.js, so no npm installation is required.
const fs = require("fs");
Modern Node.js code often also uses the promise-based API for cleaner async/await syntax.
const fs = require("fs/promises");
Asynchronous file reading is usually the better default in Node.js applications. The program starts the file read and continues running while the operating system handles the I/O work. When the file has been read, the callback receives either an error or the file contents.
const fs = require("fs");
fs.readFile("file.txt", "utf8", (error, data) => {
if (error) {
console.error("Read failed:", error.message);
return;
}
console.log(data);
});
Notice the use of "utf8". Without an encoding, Node.js returns a buffer instead of a text string. If you want to read a plain text file, specifying the encoding makes the output easier to work with.
A synchronous file read blocks execution until the operation finishes. This means the rest of the program waits. While that is not ideal for most servers, it can still be fine in tiny scripts, setup tasks, or command-line tools.
const fs = require("fs");
const data = fs.readFileSync("file.txt", "utf8");
console.log(data);
If you are building a web server or API, prefer asynchronous methods so the application can continue handling other requests while file operations are in progress.
fs.writeFile() creates a new file if it does not exist, or overwrites the existing file if it does exist. This is important to remember, because accidental overwriting is a common beginner mistake.
const fs = require("fs");
const content = "Node.js can write to files.";
fs.writeFile("file.txt", content, error => {
if (error) {
console.error("Write failed:", error.message);
return;
}
console.log("Content written successfully!");
});
const fs = require("fs");
const content = "Node.js can write to files.";
fs.writeFileSync("my_file.txt", content);
console.log("Content written successfully!");
If you want to add new content to the end of a file instead of replacing the existing content, use appendFile(). This is useful for logging, activity history, report generation, and audit trails.
const fs = require("fs");
fs.appendFile("log.txt", "New log entry\n", error => {
if (error) {
console.error("Append failed:", error.message);
return;
}
console.log("Log entry added");
});
Opening a file is a lower-level operation where Node.js returns a file descriptor. In many simple use cases, developers read or write files directly without calling open() first. Still, it is useful to understand that file descriptors exist, especially when working with advanced file operations or streams.
const fs = require("fs");
fs.open("file.txt", "r", (error, fd) => {
if (error) {
console.error("Open failed:", error.message);
return;
}
console.log("File opened. Descriptor:", fd);
});
The second argument, "r", means the file is opened for reading. Other flags such as "w" and "a" are used for writing and appending.
To remove a file, use unlink(). This is a destructive action, so your application should usually confirm that the target exists or that the delete request is valid before removing it.
const fs = require("fs");
fs.unlink("old-file.txt", error => {
if (error) {
console.error("Delete failed:", error.message);
return;
}
console.log("File deleted successfully");
});
Renaming is another very common task, especially for uploads, backups, and report generation.
const fs = require("fs");
fs.rename("draft.txt", "final.txt", error => {
if (error) {
console.error("Rename failed:", error.message);
return;
}
console.log("File renamed successfully");
});
The file system module can also create, read, and remove directories. This is useful for organizing uploads, logs, temporary files, or generated reports.
const fs = require("fs");
fs.mkdir("reports", { recursive: true }, error => {
if (error) {
console.error("Create directory failed:", error.message);
return;
}
console.log("Directory created");
});
fs.readdir(".", (error, files) => {
if (error) {
console.error("Read directory failed:", error.message);
return;
}
console.log(files);
});
The option { recursive: true } allows nested folders to be created if needed. This makes directory setup easier in many applications.
Sometimes a program must check whether a file is present before reading, deleting, or replacing it. The promise-based access() method is commonly used for this kind of validation.
const fs = require("fs/promises");
async function checkFile() {
try {
await fs.access("file.txt");
console.log("File exists");
} catch (error) {
console.log("File does not exist or cannot be accessed");
}
}
checkFile();
fs/promises with async/awaitModern Node.js applications often use the promise-based API because it works naturally with async/await. This style avoids nested callbacks and usually reads more cleanly.
const fs = require("fs/promises");
async function loadContent() {
try {
const data = await fs.readFile("file.txt", "utf8");
console.log(data);
} catch (error) {
console.error("Read failed:", error.message);
}
}
loadContent();
This approach is now very common in production code because it keeps asynchronous file logic readable while still avoiding blocking behavior.
readFile() loads the entire file into memory. That is fine for small files, but not always ideal for very large files. In those cases, streams are usually a better option because they process data in chunks.
const fs = require("fs");
const stream = fs.createReadStream("big-file.txt", "utf8");
stream.on("data", chunk => {
console.log("Received chunk:", chunk.length);
});
stream.on("end", () => {
console.log("Finished reading file");
});
Streams are very important in Node.js for large files, file uploads, downloads, and efficient data transfer. They keep memory usage lower compared with reading everything at once.
path.join() with File OperationsThe file system module is often used together with the path module. This combination helps build reliable file paths relative to the current script rather than depending on where the terminal was opened.
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "data", "users.json");
fs.readFile(filePath, "utf8", (error, data) => {
if (error) {
console.error(error.message);
return;
}
console.log(data);
});
A common mistake is forgetting to specify an encoding when reading a text file and then being confused when a buffer is returned. Another is using synchronous file methods in a server application where blocking behavior can slow down request handling. Beginners also often overwrite files accidentally with writeFile() when they really meant to append. The original short example on this page also had a variable mismatch bug between error and err, which is exactly the kind of small detail that can break file code quickly.
Another frequent issue is using relative file names without considering the current working directory. If the app is started from a different folder, paths like "file.txt" may fail unexpectedly. Combining __dirname with path.join() usually makes filesystem code more reliable.
Think of the file system module as Node.js's bridge to files and folders on disk. If your program needs to persist logs, export reports, read configuration, manage uploads, or inspect the contents of a folder, the fs module is the standard tool for the job. Once you are comfortable with reading, writing, appending, deleting, and path handling, a large part of everyday Node.js backend work becomes much easier.
fs module is built into Node.js and supports reading, writing, appending, renaming, deleting, and inspecting files.
fs/promises for cleaner async/await-based code.
writeFile() overwrites existing content, while appendFile() adds to the end.
createReadStream() for large files instead of loading everything into memory.
fs with path.join(__dirname, ...) for reliable project-relative paths.
Explore 500+ free tutorials across 20+ languages and frameworks.