Skip to content

What Is the Node.js fs (File System) Module?

The built-in Node.js file system module helps us store, access, and manage data on our operating system. Commonly used features of the fs module include fs.readFile to read data from a file, fs.writeFile to write data to a file and replace the file if it already exists, fs.watchFile to get notified of changes, and fs.appendFile to append data to a file. The fs core module is available in every Node.js project without having to install it.

By the end of this tutorial, you should be able to:

  • Understand common uses for the Node file system module
  • Learn about relative file paths when using the fs module
  • Asynchronous vs synchronous fs methods in Node
  • Have a good overview of the new promise-based version of the fs module
  • Understand when we can’t rely on the Node fs module

Goal

  • Understand how the fs module is used to access and interact with a file system.

Prerequisites

  • None

Watch

About the fs module

Node.js provides a built-in module for working with the file system of the computer running your code. The fs core module is available from every Node.js project without having to install it. If your program needs to read/write, or otherwise interact with, files saved to disk, the fs module is often what you want to use.

To use the fs module, you need to require it in your code:

const fs = require("fs");

The fs module gives us access to useful functions for watching for file changes, reading files, writing to files, creating directories, and more.

Some of the most commonly used methods fs exposes are:

You can learn more about all the fs methods in the Node.js Documentation.

Relative file paths

Most of the fs methods take a file path as their first argument. If you use a relative file path (e.g. ./output.txt) the path will be resolved from the perspective of the current working directory when the script is run. Relative paths are prefixed with either a single ., indicating “from the current directory”, or two dots .., which indicates “parent directory”.

When using relative paths with fs, the path is not relative to where the script is located, it is relative to the directory you were in when you ran the node command to execute the script. Note that this behavior is different than that of require. If you’re not paying attention to how you specify relative path names, you may end up being surprised by the results when files are written to or read from locations you weren’t expecting.

In order to specify a path relative to the file that’s being executed, you can use the special keyword __dirname which is available in the scope of every file.

The __dirname keyword expands to the absolute path of the directory of the file it is used in. So if you use __dirname from a file located at /foo/bar/script.js, the value would be /foo/bar.

You can then create a full path by using the value returned by __dirname and the relative path to the file. To join the paths together, we use another core Node.js module called path. Using the path.join method allows us to create cross-platform filepaths.

const fs = require("fs");
const path = require("path");
fs.writeFile(
"./out.txt",
"My path is relative to the current working directory! aka process.cwd()",
err => console.error(err)
);
fs.writeFile(
path.join(__dirname, "/out.txt"),
"My path is relative to the directory this script inhabits!",
err => console.error(err)
);

You can also combine __dirname and a relative path, to make the path lookup relative to the directory of the current file.

const fs = require("fs");
const path = require("path");
fs.writeFile(
path.join(__dirname, "../../out.txt"),
"This path starts at __dirname, and then goes up two levels from there",
err => console.error(err)
);

Asynchronous v.s. synchronous fs methods

By default, the functions provided by fs are asynchronous and callback-based. But many of these functions have synchronous versions as well, such as readFileSync and writeFileSync. Typically, all I/O operations in Node.js are done asynchronously to avoid blocking the event loop and making your program unresponsive while I/O heavy tasks run.

If it’s best practice to use asynchronous code when working with the filesystem and other I/O heavy operations, then you should probably always use the asynchronous versions, right? Well, usually.

It is best practice to use asynchronous versions of fs methods to deal with accessing the filesystem, but there are some occasions when you actually do want to block the execution of your program until after you’re done accessing the filesystem.

Specifically, if you are loading configuration data from a file on startup, you might want to make sure that the configuration data has been loaded before the rest of your program runs.

In most cases, you don’t want to be synchronously accessing the filesystem often because it will indeed affect the performance of your application. But on the rare case when you need to infrequently (e.g. just once on startup) prevent the rest of your code from running before a file has been read or created, the synchronous versions of the fs methods are really useful.

const fs = require("fs");
const config = JSON.parse(fs.readFileSync("./config.json"));
// continue running code, with config now available

Outside of that particular usecase, it is recommended you always use the asynchronous methods to access the filesystem.

New promise support

Promise-based versions of the fs module methods were added in version 10 of Node.js (in 2018). The support is still experimental in version 10, and you’ll get a console warning noting as much when using it. In Node.js version 11 and above, the promise support is no longer experimental and you can use it without the warnings.

The promise-based fs methods work the same as their callback-based equivalents and the names are the same as well. Notably, there aren’t synchronous versions of these methods. If you need the synchronous versions, they are available from the main non-promise-based fs module.

Access the promise-based version of the fs module like so:

const fs = require("fs").promises;

Instead of taking a callback, these methods return a Promise.

const fs = require("fs").promises;
const writePromise = fs.writeFile("./out.txt", "Hello, World");
writePromise
.then(() => console.log("success!"))
.catch(err => console.error(err));

When can’t we rely on the filesystem?

Depending on where our code is running, we don’t always have access to a persistent filesystem. When developing locally on your own machine, you can easily persist files and access them again later. But in certain environments in the cloud, we don’t have access to a persistent filesystem.

Heroku, for example, is a popular cloud host that uses an ephemeral filesystem. Files you write to the disk are persisted until your instance powers down to sleep during periods of inactivity, and on wake your previously saved files will be gone. This only happens with files your program creates in the course of running, not your source code. AWS Lambda functions work similarly; files created by your program will only be persisted for a short time.

For reasons like this, storing long-lived state or user uploads as files created at runtime is not recommended when running your application on a host with an ephemeral filesystem. The host you choose should be able to tell you if the filesystem under your application is persistent or ephemeral.

Recap

In this tutorial, we discussed how the fs module is used to access and interact with the file system. You learned that relative file paths are interpreted from the directory a program was run from, how synchronous versions of the fs methods can be useful for reading config data on startup, and that there are new promise-based versions of the fs methods available.

Further your understanding

  • Explain to a co-worker the kinds of tasks you can accomplish with the fs module.
  • How are relative paths different when using require vs fs methods?
  • Can you think of alternatives to storing state in files when using an ephemeral filesystem?

Additional resources