How to use the File System in Node.js

Share this article

How to use the File System in Node.js

Web applications don’t always need to write to the file system, but Node.js provides a comprehensive application programming interface (API) for doing so. It may be essential if you’re outputting debugging logs, transferring files to or from a server, or creating command line tools.

Reading and writing files from code is not necessarily difficult, but your application will be more robust if you do the following:

  1. Ensure it’s cross-platform

    Windows, macOS, and Linux handle files differently. For example, you use a forward slash / to separate directories in macOS and Linux, but Windows uses a backslash \ and bans certain file name characters such as : and ?.

  2. Double-check everything!

    Users or other apps could delete a file or change access permissions. Always check for such issues and handle errors effectively.

Table of Contents

The Node.js fs Module

The Node.js fs module provides methods for managing files and directories. If you’re using other JavaScript runtimes:

All JavaScript runtimes run on a single processing thread. The underlying operating system handles operations such as file reading and writing, so the JavaScript program continues to run in parallel. The OS then alerts the runtime when a file operation is complete.

The fs documentation provides a long list of functions, but there are three general types with similar functions, which we’ll look at next.

1. Callback functions

These functions take a completion callback function as an argument. The following example passes an inline function which outputs the content of myfile.txt. Assuming no errors, its content displays after end of program appears in the console:

import { readFile } from 'node:fs';

readFile('myfile.txt', { encoding: 'utf8' }, (err, content) => {
  if (!err) {
    console.log(content);
  }
});

console.log('end of program');

Note: the { encoding: 'utf8' } parameter ensures Node.js returns a string of text content rather than a Buffer object of binary data.

This becomes complicated when you need to run one after another and descend into nested callback hell! It’s also easy to write callback functions which look correct but cause memory leaks that are difficult to debug.

In most cases, there’s little reason to use callbacks today. Few of the examples below use them.

2. Synchronous functions

The “Sync” functions effectively ignore Node’s non-blocking I/O and provide synchronous APIs like you’d find in other programming languages. The following example outputs the content of myfile.txt before end of program appears in the console:

import { readFileSync } from 'node:fs';

try {
  const content = readFileSync('myfile.txt', { encoding: 'utf8' });
  console.log(content);
}
catch {}

console.log('end of program');

It looks easier, and I’d never say don’t use Sync … but, erm … don’t use Sync! It halts the event loop and pauses your application. That may be fine in a CLI program when loading a small initialization file, but consider a Node.js web application with 100 concurrent users. If one user requests a file which takes one second to load, they wait one second for a response — and so do all the other 99 users!

There’s no reason to use synchronous methods when we have promise functions.

3. Promise functions

ES6/2015 introduced promises. They’re syntactical sugar on callbacks to provide a sweeter, easier syntax, especially when used with async/await. Node.js also introduced a ‘fs/promises’ API which looks and behaves in a similar way to the synchronous function syntax but remains asynchronous:

import { readFile } from 'node:fs/promises';

try {
  const content = await readFile('myfile.txt', { encoding: 'utf8' });
  console.log(content);
}
catch {}

console.log('end of program');

Note the use of the 'node:fs/promises' module and the await before readFile().

Most examples below use the promise-based syntax. Most do not include try and catch for brevity, but you should add those blocks to handle errors.

ES module syntax

The examples in this tutorial also use ES Modules (ESM) import rather than the CommonJS require. ESM is the standard module syntax supported by Deno, Bun, and browser runtimes.

To use ESM in Node.js, either:

  • name your JavaScript files with a .mjs extension
  • use an --import=module switch on the command line — such as node --import=module index.js, or
  • if you have a project package.json file, add a new "type": "module" setting

You can still use CommonJS require should you need to.

Reading Files

There are several functions for reading files, but the simplest is to read a whole file into memory using readFile, as we saw in the example above:

import { readFile } from 'node:fs/promises';
const content = await readFile('myfile.txt', { encoding: 'utf8' });

The second options object can also be a string. It defines the encoding: set 'utf8' or another text format to read the file content into a string.

Alternatively, you can read lines one at a time using the readLines() method of the filehandle object:

import { open } from 'node:fs/promises';

const file = await open('myfile.txt');

for await (const line of file.readLines()) {
  console.log(line);
}

There are also more advanced options for reading streams or any number of bytes from a file.

Handling File and Directory Paths

You’ll often want to access files at specific absolute paths or paths relative to the Node application’s working directory. The node:path module provides cross-platform methods to resolve paths on all operating systems.

The path.sep property returns the directory separator symbol — \ on Windows or / on Linux or macOS:

import * as path from 'node:path';

console.log( path.sep );

But there are more useful properties and functions. join([…paths]) joins all path segments and normalizes for the OS:

console.log( path.join('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/

resolve([…paths]) is similar but returns the full absolute path:

console.log( path.resolve('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
C:\project\node\example2\myfile.txt on Windows
*/

normalize(path) resolves all directory .. and . references:

console.log( path.normalize('/project/node/example1/../example2/myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/

relative(from, to) calculates the relative path between two absolute or relative paths (based on Node’s working directory):

console.log( path.relative('/project/node/example1', '/project/node/example2') );
/*
../example2 on macOS/Linux
..\example2 on Windows
*/

format(object) builds a full path from an object of constituent parts:

console.log(
  path.format({
    dir: '/project/node/example2',
    name: 'myfile',
    ext: 'txt'
  })
);
/*
/project/node/example2/myfile.txt
*/

parse(path) does the opposite and returns an object describing a path:

console.log( path.parse('/project/node/example2/myfile.txt') );
/*
{
  root: '/',
  dir: '/project/node/example2',
  base: 'myfile.txt',
  ext: '.txt',
  name: 'myfile'
}
*/

Getting File and Directory Information

You often need to get information about a path. Is it a file? Is it a directory? When was it created? When was it last modified? Can you read it? Can you append data to it?

The stat(path) function returns a Stats object containing information about a file or directory object:

import { stat } from 'node:fs/promises';

const info = await stat('myfile.txt');
console.log(info);
/*
Stats {
  dev: 4238105234,
  mode: 33206,
  nlink: 1,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  ino: 3377699720670299,
  size: 21,
  blocks: 0,
  atimeMs: 1700836734386.4246,
  mtimeMs: 1700836709109.3108,
  ctimeMs: 1700836709109.3108,
  birthtimeMs: 1700836699277.3362,
  atime: 2023-11-24T14:38:54.386Z,
  mtime: 2023-11-24T14:38:29.109Z,
  ctime: 2023-11-24T14:38:29.109Z,
  birthtime: 2023-11-24T14:38:19.277Z
}
*/

It also provides useful methods, including:

const isFile = info.isFile(); // true
const isDirectory = info.isDirectory(); // false

The access(path) function tests whether a file can be accessed using a specific mode set via a constant. If the accessibility check is successful, the promise fulfills with no value. A failure rejects the promise. For example:

import { access, constants } from 'node:fs/promises';

const info = {
  canRead: false,
  canWrite: false,
  canExec: false
};

// is readable?
try {
  await access('myfile.txt', constants.R_OK);
  info.canRead = true;
}
catch {}

// is writeable
try {
  await access('myfile.txt', constants.W_OK);
  info.canWrite = true;
}
catch {}

console.log(info);
/*
{
  canRead: true,
  canWrite: true
}
*/

You can test more than one mode, such as whether a file is both readable and writeable:

await access('myfile.txt', constants.R_OK | constants.W_OK);

Writing Files

writeFile() is the simplest function to asynchronously write a whole file replacing its content if it already exists:

import { writeFile } from 'node:fs/promises';
await writeFile('myfile.txt', 'new file contents');

Pass the following arguments:

  1. the file path
  2. the file content — can be a String, Buffer, TypedArray, DataView, Iterable, or Stream
  3. an optional third argument can be a string representing the encoding (such as 'utf8') or an object with properties such as encoding and signal to abort the promise.

A similar appendFile() function adds new content to the end of the current file, creating that file if it does not exist.

For the adventurous, there is a file handler write() method which allows you to replace content inside a file at a specific point and length.

Creating Directories

The mkdir() function can create full directory structures by passing an absolute or relative path:

import { mkdir } from 'node:fs/promises';

await mkdir('./subdir/temp', { recursive: true });

You can pass two arguments:

  1. the directory path, and
  2. an optional object with a recursive Boolean and mode string or integer

Setting recursive to true creates the whole directory structure. The example above creates subdir in the current working directory and temp as a subdirectory of that. If recursive were false (the default) the promise would reject if subdir were not already defined.

The mode is the macOS/Linux user, group, and others permission with a default of 0x777. This is not supported on Windows and ignored.

The similar .mkdtemp() function is similar and creates a unique directory typically for temporary data storage.

Reading Directory Contents

.readdir() reads the content of a directory. The promise fulfills with an array containing all file and directory names (except for . and ..). The name is relative to the directory and does not include the full path:

import { readdir } from 'node:fs/promises';

const files = await readdir('./'); // current working directory
for (const file of files) {
  console.log(file);
}

/*
file1.txt
file2.txt
file3.txt
index.mjs
*/

You can pass an optional second parameter object with the following properties:

  • encoding — the default is an array of utf8 strings
  • recursive — set true to recursively fetch all files from all subdirectories. The file name will include the subdirectory name(s). Older editions of Node.js may not provide this option.
  • withFileType — set true to return an array of fs.Dirent objects which includes properties and methods including .name, .path, .isFile(), .isDirectory() and more.

The alternative .opendir() function allows you to asynchronously open a directory for iterative scanning:

import { opendir } from 'node:fs/promises';

const dir = await opendir('./');
for await (const entry of dir) {
  console.log(entry.name);
}

Deleting Files and Directories

The .rm() function removes a file or directory at a specified path:

import { rm } from 'node:fs/promises';

await rm('./oldfile.txt');

You can pass an optional second parameter object with the following properties:

  • force — set true to not raise an error when the path does not exist
  • recursive — set true to recursively delete a directory and contents
  • maxRetries — make a number of retries when another process has locked a file
  • retryDelay — the number of milliseconds between retries

The similar .rmdir() function only deletes directories (you can’t pass a file path). Similarly, .unlink() only deletes files or symbolic links (you can’t pass a directory path).

Other File System Functions

The examples above illustrate the most-used options for reading, writing, updating, and deleting files and directories. Node.js also provides further, lesser-used options such as copying, renaming, changing ownership, changing permissions, changing date properties, creating symbolic links, and watching for file changes.

It may be preferable to use the callback-based API when watching for file changes, because it’s less code, easier to use, and can’t halt other processes:

import { watch } from 'node:fs';

// run callback when anything changes in directory
watch('./mydir', { recursive: true }, (event, file) => {

  console.log(`event: ${ event }`);

  if (file) {
    console.log(`file changed: ${ file }`);
  }

});

// do more...

The event parameter received by the callback is either 'change' or 'rename'.

Summary

Node.js provides a flexible and cross-platform API to manage files and directories on any operating system where you can use the runtime. With a little care, you can write robust and portable JavaScript code that can interact with any file system.

For more information, refer to the Node.js fs and path documentation. Other useful libraries include:

  • OS to query operating oystem information
  • URL to parse a URL, perhaps when mapping to and from file system paths
  • Stream for handling large files
  • Buffer and TypedArray objects to handle binary data
  • Child processes to spawn a subprocess to handle long-running or complex file manipulation functions.

You can also find higher-level file system modules on npm, but there’s no better experience than writing your own.

FAQs on Accessing the File System in Node.js

What is the File System module in Node.js?

The File System module, often referred to as fs, is a core module in Node.js that provides methods and functionality for interacting with the file system, including reading and writing files.

How can I include the fs module in a Node.js script?

You can include the fs module by using the require statement, like this: const fs = require('fs');. This makes all fs methods available in your script.

What is the difference between synchronous and asynchronous file operations in Node.js?

Synchronous file operations block the Node.js event loop until the operation is completed, while asynchronous operations do not block the event loop, allowing your application to remain responsive. Asynchronous operations are typically recommended for I/O tasks.

How do I read the contents of a file in Node.js using the fs module?

You can use the fs.readFile() method to read the contents of a file. Provide the file path and a callback function to handle the data once it’s read.

What is the purpose of the callback function when working with the fs module in Node.js?

Callback functions in fs operations are used to handle the result of asynchronous file operations. They are called when the operation is complete, passing any errors and data as arguments.

How can I check if a file exists in Node.js using the fs module?

You can use the fs.existsSync() method to check if a file exists at a specified path. It returns true if the file exists and false if it doesn’t.

What is the fs.createReadStream() method, and when is it useful?

fs.createReadStream() is used for reading large files efficiently. It creates a readable stream for the specified file, allowing you to read and process data in smaller, manageable chunks.

Can I use the fs module to create and write to a new file in Node.js?

Yes, you can use the fs.writeFile() or fs.createWriteStream() methods to create and write to a new file. These methods allow you to specify the file path, content, and options for writing.

How do I handle errors when working with the fs module in Node.js?

You should always handle errors by checking the error parameter in the callback function provided to asynchronous fs methods or by using try/catch blocks for synchronous operations.

Is it possible to delete a file using the fs module in Node.js?

Yes, you can use the fs.unlink() method to delete a file. Provide the file path and a callback function to handle the result.

Can I use the fs module to work with directories and folder structures in Node.js?

Yes, the fs module provides methods to create, read, and manipulate directories, including creating directories, listing their contents, and removing directories.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

file system apiIntermediateLearn-Node-JSnode.jsserver-side
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week