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.
Modern Node.js code often also uses the promise-based API for cleaner async/await syntax.
const fs = require("fs");
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.
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.
const fs = require("fs");
fs.readFile("file.txt", "utf8", (error, data) => {
if (error) {
console.error("Read failed:", error.message);
return;
}
console.log(data);
});
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.
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.
const fs = require("fs");
const data = fs.readFileSync("file.txt", "utf8");
console.log(data);
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.
The second argument, "r", means the file is opened for reading. Other flags such as "w" and "a" are used for writing and appending.
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);
});
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.
The option { recursive: true } allows nested folders to be created if needed. This makes directory setup easier in many applications.
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);
});
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();
Modern 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.
This approach is now very common in production code because it keeps asynchronous file logic readable while still avoiding blocking behavior.
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();
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.
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.
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");
});
The 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.
Use `node:fs/promises` for ordinary asynchronous reads, writes, metadata, and directory operations. Use streams for files too large to hold comfortably in memory or when data should flow with backpressure. Keep synchronous APIs for short startup or command-line work where blocking is deliberate, not inside a shared HTTP request path.
Pass explicit encodings when text is expected; otherwise Node returns buffers. Bound file size before `readFile`, handle partial and aborted work, and close file handles in `finally` or through APIs that own closure. Concurrent reads and writes need an application rule because checking existence and acting later creates a race.
For durable replacement, write a temporary file in the same filesystem, flush according to the durability need, and rename it atomically where the platform permits. Do not overwrite configuration or user data directly and assume a process crash cannot leave a partial file.
Never concatenate an untrusted filename onto a storage directory. Decode once, reject absolute paths and null bytes, resolve against an approved root, account for platform separators, and confirm the final canonical target remains inside the root. Symlinks can redirect a path after a lexical prefix check, so high-risk code needs a deliberate symlink policy.
Open files with the least permissions required and avoid predictable temporary names. Restrict upload type and size, generate server-owned names, keep private content outside public web roots, and scan risky formats when the product requires it. File extensions and client MIME headers are not proof of content.
Map missing files, permission denial, already-existing targets, and storage exhaustion into safe domain outcomes. Log operation name and a non-sensitive identifier rather than private content or absolute infrastructure paths. Test traversal, symlinks, Unicode names, Windows and POSIX separators, large files, full disks, and cleanup after failure.
readFileSync blocks the event loop until the disk operation finishes.
Check the path relative to the process working directory, not only relative to the source file you are editing. ENOENT often means the program is running from a different folder than expected.
readFile loads the entire file into memory before your code can respond. Streams process data in chunks, which reduces memory pressure and supports backpressure when the receiver is slower than the disk or network.
Explore 500+ free tutorials across 20+ languages and frameworks.