Working with files in Node.js (Part 1)
Node.js includes several core modules, one of the most commonly used is the fs
module - short for “File System”.
This module enables developers to interact with the file system, allowing to read, write and modify files directly
within Node.js applications. In this article, we’ll explore how to handle files both synchronous and asynchronous
methods provided by the fs
module.
Operating with file synchronously
To get started, let’s create a sample file that we can read and manipulate. You can either create a file manually or use the following command in your Terminal.
echo Hello World > hello.txt
Next, let’s create the script named read-write-sync.js
that will read and modify the content of a file. You can
do this with the command below.
touch read-write-sync.js
In this example, the fs
module will be imported from the core Node.js File System module. Similarly, the core
path
module provides APIs for working with file and directory paths. we’ll synchronously read the file called
hello.txt
. Then, we use the fs.readFileSync()
function to read the file. We pass the file path and the
encoding UTF-8 as the parameter to this function. This encoding parameter is optional. When the encoding parameter
is omitted, the function will automatically return a Buffer object.
const fs = require("fs");
const path = require("path");
const filePath = path.join(process.cwd(), "hello.txt");
const contents = fs.readFileSync(filePath, "utf8");
console.log("File Contents", contents);
The
process.cwd()
method is a function of the global process object that provides the current working directory of the Node.js process. This program expects that thehello.txt
file is located in the same directory as the program.
At this point, we can modify the file’s content and convert the text into uppercase. We use the writeFileSync()
function to modify the file. To manipulate of the file contents, we use the toUpperCase()
string method
to transform the content. We pass the two parameters to the fs.writeFileSync()
function. The first parameter
is the file path we would like to modify and the second parameter is the updated file contents. We’ll also add a
log statement afterward indicating that the file has been modified.
const uppercase = contents.toUpperCase();
fs.writeFileSync(filePath, uppercase);
console.log("File updated.");
Next, we can execute the program using the following script.
node read-write-sync.js
To verify the contents we updated, your can either open the file or use the cat
command in your Terminal to show
the contents of hello.txt
.
cat hello.txt
HELLO WORLD
Finally, we have a program that will read the contents of hello.txt
and transform the content into uppercase.
However, the synchronous methods used here block the event loop until the operation is complete. Node.js enables the non-blocking I/O model which we can perform operations to be asynchronous. So, there are three ways to handle asynchronous code in Node.js: callbacks, Promises and async/await syntax. In this section, we’ll use callbacks to handle files asynchronously.
Operating with files asynchronously
Node.js is designed around a non-blocking, asynchronous I/O model. The asynchronous versions of file operations
enable the program to keep executing while waiting for operations to complete. In the earlier program, the file
was written using the synchronous functions available in the fs
module.
const fs = require("fs");
const path = require("path");
const filePath = path.join(process.cwd(), "hello.txt");
const contents = fs.readFileSync(filePath, "utf-8");
console.log("File Contents", contents);
const upperContents = contents.toUpperCase();
fs.writeFileSync(filePath, upperContents);
console.log("File Updated.");
This means that the program was blocked while waiting for the readFileSync()
and writeFileSync()
operations to
complete. This program can be rewritten to make use of asynchronous APIs. The asynchronous version of
readFileSync()
is readFile()
while writeFileSync()
corresponds to writeFile()
. The asynchronous functions
require a callback function to be passed to them. The callback function contains the code that we want to be
executed once the asynchronous task is completed.
Let’s rewrite our previous program using the asynchronous functions and callbacks.
const fs = require("fs");
const path = require("path");
const filePath = path.join(process.cwd(), "hello.txt");
fs.readFile(filePath, "utf8", (err, contents) => {
if (err) {
return console.log("Failed to read file: ", err);
}
console.log("File Contents", contents);
const upperContents = contents.toUpperCase();
fs.writeFile(filePath, upperContents, (err, contents) => {
if (err) {
return console.log("Failed to write file: ", err);
}
console.log("File Updated.");
});
});
Now, we have implemented an asynchronous function that invokes another asynchronous function. However, it is not recommended to use too many nested callbacks as it can negatively impact the readability of the code. Consider the following to see how having too many nested callbacks impedes the readability of the code which is known as “callback hell”.
fs.readFile(filePath, "utf8", (err, data) => {
if (err) return;
fs.writeFile(filePath, data.toUpperCase(), (err) => {
if (err) return;
fs.appendFile(filePath, "\nUpdated!", (err) => {
if (err) return;
console.log("All done!");
});
});
});
There are some approaches that can be taken to avoid too many nested callbacks. One approach to split callbacks
into explicity named functions. In our file, we can extract writeFile()
within its own named function called
updateFile()
.
const fs = require("fs");
const path = require("path");
const filePath = path.join(process.cwd(), "hello.txt");
fs.readFile(filePath, "utf8", (err, contents) => {
if (err) {
return console.log("Failed to read file: ", err);
}
console.log("File Contents", contents);
const upperContents = contents.toUpperCase();
updateFile(filePath, contents);
});
function updateFile(filePath, contents) {
fs.writeFile(filePath, upperContents, (err) => {
if (err) {
return console.log("Failed to write file: ", err);
}
console.log("File Updated.");
});
}
In this article, we explored how to work with the Node.js fs
module to read and write files using both
synchronous and asynchronous approaches. While callbacks based asynchronous are powerful, they can quickly become
difficult to manage in complex applications. As a next step, we’ll move beyond callbacks and explore a cleaner,
more modern approach to synchronous file handling using the Promise APIs. This allows us to write more readable
and maintainable code using async/await
syntax. We’ll explore this approach in detail in the next article:
Working with files in Node.js (Part 2).