graceful-fs vs rimraf vs fs-extra vs mkdirp vs node-fs
Node.js File System Utilities: Architecture and Selection Guide
graceful-fsrimraffs-extramkdirpnode-fsSimilar Packages:

Node.js File System Utilities: Architecture and Selection Guide

fs-extra, graceful-fs, mkdirp, node-fs, and rimraf are utilities designed to extend or simplify Node.js native file system operations. fs-extra adds missing methods like copy and move while maintaining async support. graceful-fs patches the native fs module to handle file descriptor limits and Windows file locking issues. mkdirp creates nested directories recursively. rimraf provides a robust way to delete files and folders recursively. node-fs is largely obsolete as modern Node.js versions include these features natively.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
graceful-fs144,319,0791,30332.5 kB513 years agoISC
rimraf137,962,3305,842262 kB82 months agoBlueOak-1.0.0
fs-extra09,61757.7 kB132 months agoMIT
mkdirp0199107 kB13 years agoMIT
node-fs068-113 years ago-

Node.js File System Utilities: Architecture and Selection Guide

When building Node.js applications, tooling, or build scripts, interacting with the file system is a common requirement. The native fs module covers the basics, but edge cases like recursive deletion, nested directory creation, and file descriptor limits often require extra help. The packages fs-extra, graceful-fs, mkdirp, node-fs, and rimraf address these gaps. Let's examine how they differ and when to use them.

📂 Creating Directories: Native vs Utility

Creating nested directories is a frequent task in scaffolding tools or build pipelines.

mkdirp was the standard solution for years before Node.js added native support. It ensures all parent directories exist before creating the target folder.

// mkdirp: Create nested directories
const mkdirp = require('mkdirp');

await mkdirp('/tmp/foo/bar/baz');
// Creates /tmp, /tmp/foo, /tmp/foo/bar, and /tmp/foo/bar/baz

fs-extra includes a mkdirs method that behaves similarly to mkdirp but is part of a larger utility suite.

// fs-extra: Create nested directories
const fse = require('fs-extra');

await fse.mkdirs('/tmp/foo/bar/baz');
// Same result as mkdirp, integrated with other fs-extra methods

node-fs historically offered promise-based directory creation before Node v10.

// node-fs: Legacy promise-based creation
const fs = require('node-fs');

fs.mkdir('/tmp/foo/bar/baz', true, (err) => {
  // The 'true' flag enabled recursive creation in this legacy API
});

graceful-fs does not add new methods but makes native fs.mkdir more resilient to race conditions.

// graceful-fs: Patched native mkdir
const fs = require('graceful-fs');

await fs.promises.mkdir('/tmp/foo/bar/baz', { recursive: true });
// Uses native API but with better error handling for file limits

rimraf is not used for creation, but knowing its counterpart helps clarify scope.

// rimraf: Not applicable for creation
// Used only for deletion (see below)

💡 Note: In Node v10.12.0 and later, native fs.mkdir(path, { recursive: true }) replaces the need for mkdirp in most cases.

🗑️ Deleting Files and Folders: Recursive Removal

Deleting a directory tree is riskier than creating one due to file permissions and open handles.

rimraf is the industry standard for recursive deletion. It handles Windows file locking issues better than naive native scripts.

// rimraf: Robust recursive delete
const rimraf = require('rimraf');

rimraf('/tmp/foo/bar', (err) => {
  // Deletes /tmp/foo/bar and all contents safely
});

fs-extra provides remove, which is essentially a promise-wrapped rimraf functionality built-in.

// fs-extra: Promise-based remove
const fse = require('fs-extra');

await fse.remove('/tmp/foo/bar');
// Async/await friendly deletion

node-fs lacked robust recursive delete methods in older Node versions.

// node-fs: No direct recursive delete
// Required manual implementation or external tools in legacy environments

graceful-fs relies on the native fs.rm (Node v14.14.0+) but adds stability.

// graceful-fs: Native recursive remove
const fs = require('graceful-fs');

await fs.promises.rm('/tmp/foo/bar', { recursive: true, force: true });
// More stable than raw fs.rm on high-load systems

mkdirp does not handle deletion.

// mkdirp: Not applicable for deletion
// Focused solely on directory creation

📋 Extended File Operations: Copy, Move, and Write

The native fs module lacks high-level methods like copy or outputFile (write + create dir).

fs-extra shines here by adding these missing methods directly.

// fs-extra: Copy and output file
const fse = require('fs-extra');

await fse.copy('/src/file.txt', '/dest/file.txt');
await fse.outputFile('/tmp/nested/config.json', '{ "key": "value" }');
// outputFile creates parent directories automatically

rimraf focuses only on deletion, so it does not offer copy or move.

// rimraf: No copy/move support
// Single-purpose package for removal

mkdirp is strictly for directories.

// mkdirp: No file content support
// Cannot write file content, only create folders

graceful-fs does not add new methods like copy.

// graceful-fs: No new methods
// You must still use fs.copyFile or implement copy logic manually

node-fs attempted to add these but is now obsolete.

// node-fs: Legacy methods
// Modern code should use fs-extra or native fs.promises

🛡️ Stability and Error Handling

File system operations often fail due to OS limits, like running out of file descriptors (EMFILE).

graceful-fs is designed specifically to handle these errors. It queues operations when limits are hit instead of crashing.

// graceful-fs: Handle EMFILE errors
const fs = require('graceful-fs');

// Simply requiring it patches the global fs module
fs.readFile('/large/file.txt', (err, data) => {
  // Retries automatically if EMFILE occurs
});

fs-extra depends on graceful-fs internally in many versions, inheriting some stability.

// fs-extra: Inherits stability
const fse = require('fs-extra');
// Benefits from graceful-fs under the hood for read/write ops

rimraf often uses graceful-fs internally to ensure deletion doesn't fail due to locking.

// rimraf: Uses graceful-fs
// Ensures delete operations retry on Windows file locks

mkdirp handles EEXIST errors gracefully but doesn't patch global limits.

// mkdirp: Handle existing dirs
// Won't error if directory already exists, but doesn't fix EMFILE

node-fs lacks modern error handling strategies.

// node-fs: Legacy error handling
// Does not include modern retry logic for file limits

🌐 Real-World Scenarios

Scenario 1: Build Tool Cleanup

You need to delete the dist folder before every build run.

  • Best choice: rimraf
  • Why? It is battle-tested for recursive deletion across OS platforms.
const rimraf = require('rimraf');
rimraf.sync('./dist');

Scenario 2: Scaffolding a New Project

You are creating a CLI that generates a folder structure with config files.

  • Best choice: fs-extra
  • Why? You need outputFile to write configs into nested folders in one step.
const fse = require('fs-extra');
await fse.outputFile('./src/config/db.json', configData);

Scenario 3: High-Concurrency File Processor

Your app processes thousands of files simultaneously and hits EMFILE errors.

  • Best choice: graceful-fs
  • Why? It queues requests to prevent crashing when file descriptors are exhausted.
const fs = require('graceful-fs');
// Import this at the very top of your entry file

Scenario 4: Legacy Node Support (Pre-v10)

You must support old Node versions that lack fs.mkdir recursive option.

  • Best choice: mkdirp
  • Why? Native recursive mkdir is unavailable, so this polyfill is necessary.
const mkdirp = require('mkdirp');
await mkdirp('./deep/nested/path');

🌱 When to Use Native Features Instead

Modern Node.js versions (v14+) have closed the gap significantly.

  • Recursive mkdir: Use fs.mkdir(path, { recursive: true }) instead of mkdirp.
  • Recursive rm: Use fs.rm(path, { recursive: true, force: true }) instead of rimraf.
  • Promises: Use fs.promises instead of node-fs.

However, fs-extra remains valuable for methods like copy and move which are still verbose or complex with native fs.

📌 Summary Table

PackagePrimary UseNative AlternativeActive Maintenance
fs-extraExtended ops (copy, move, output)Partial (fs.promises)✅ Yes
graceful-fsStability (EMFILE, locking)No (patches fs)✅ Yes
mkdirpRecursive directory creationfs.mkdir (v10.12+)✅ Yes
node-fsLegacy promise/shimfs.promises❌ No (Obsolete)
rimrafRecursive deletionfs.rm (v14.14+)✅ Yes

💡 Final Recommendation

Think in terms of necessity and runtime version:

  • Need copy/move/outputFile? → Use fs-extra.
  • Running on old Node (< v10)? → Use mkdirp and rimraf.
  • Running on modern Node (v16+)? → Use native fs for mkdir/rm, but keep fs-extra for convenience methods.
  • Hitting file limit errors? → Install graceful-fs and require it at your app entry point.
  • Avoid node-fs entirely in new codebases.

These tools solve specific pain points in file management. By matching the tool to your Node version and specific operation needs, you can keep your file system code robust and maintainable.

How to Choose: graceful-fs vs rimraf vs fs-extra vs mkdirp vs node-fs

  • graceful-fs:

    Choose graceful-fs if you are building a CLI tool or long-running process that frequently opens many files and risks hitting EMFILE errors. It is best used as a foundational patch early in your application entry point to stabilize file operations on all platforms.

  • rimraf:

    Choose rimraf when you need to delete files or directories recursively with robust handling of Windows file permissions and locking. It is the standard choice for cleanup tasks in build pipelines, though native fs.rm with { recursive: true } is a viable alternative in Node v14.14.0+.

  • fs-extra:

    Choose fs-extra when you need a drop-in replacement for the native fs module that includes extra methods like copy, move, and outputFile. It is ideal for build scripts, tooling, or server-side applications where convenience and promise support are priorities over minimizing dependencies.

  • mkdirp:

    Choose mkdirp if you are on a Node.js version older than v10.12.0 and need to create nested directories. For modern projects, prefer the native fs.mkdir with { recursive: true } to avoid adding a dependency for functionality now built into the runtime.

  • node-fs:

    Do not choose node-fs for new projects. It is a legacy shim that provided promise-based or extended file system methods before they were standardized. Modern Node.js versions include these features natively, making this package unnecessary and potentially confusing.

README for graceful-fs

graceful-fs

graceful-fs functions as a drop-in replacement for the fs module, making various improvements.

The improvements are meant to normalize behavior across different platforms and environments, and to make filesystem access more resilient to errors.

Improvements over fs module

  • Queues up open and readdir calls, and retries them once something closes if there is an EMFILE error from too many file descriptors.
  • fixes lchmod for Node versions prior to 0.6.2.
  • implements fs.lutimes if possible. Otherwise it becomes a noop.
  • ignores EINVAL and EPERM errors in chown, fchown or lchown if the user isn't root.
  • makes lchmod and lchown become noops, if not available.
  • retries reading a file if read results in EAGAIN error.

On Windows, it retries renaming a file for up to one second if EACCESS or EPERM error occurs, likely because antivirus software has locked the directory.

USAGE

// use just like fs
var fs = require('graceful-fs')

// now go and do stuff with it...
fs.readFile('some-file-or-whatever', (err, data) => {
  // Do stuff here.
})

Sync methods

This module cannot intercept or handle EMFILE or ENFILE errors from sync methods. If you use sync methods which open file descriptors then you are responsible for dealing with any errors.

This is a known limitation, not a bug.

Global Patching

If you want to patch the global fs module (or any other fs-like module) you can do this:

// Make sure to read the caveat below.
var realFs = require('fs')
var gracefulFs = require('graceful-fs')
gracefulFs.gracefulify(realFs)

This should only ever be done at the top-level application layer, in order to delay on EMFILE errors from any fs-using dependencies. You should not do this in a library, because it can cause unexpected delays in other parts of the program.

Changes

This module is fairly stable at this point, and used by a lot of things. That being said, because it implements a subtle behavior change in a core part of the node API, even modest changes can be extremely breaking, and the versioning is thus biased towards bumping the major when in doubt.

The main change between major versions has been switching between providing a fully-patched fs module vs monkey-patching the node core builtin, and the approach by which a non-monkey-patched fs was created.

The goal is to trade EMFILE errors for slower fs operations. So, if you try to open a zillion files, rather than crashing, open operations will be queued up and wait for something else to close.

There are advantages to each approach. Monkey-patching the fs means that no EMFILE errors can possibly occur anywhere in your application, because everything is using the same core fs module, which is patched. However, it can also obviously cause undesirable side-effects, especially if the module is loaded multiple times.

Implementing a separate-but-identical patched fs module is more surgical (and doesn't run the risk of patching multiple times), but also imposes the challenge of keeping in sync with the core module.

The current approach loads the fs module, and then creates a lookalike object that has all the same methods, except a few that are patched. It is safe to use in all versions of Node from 0.8 through 7.0.

v4

  • Do not monkey-patch the fs module. This module may now be used as a drop-in dep, and users can opt into monkey-patching the fs builtin if their app requires it.

v3

  • Monkey-patch fs, because the eval approach no longer works on recent node.
  • fixed possible type-error throw if rename fails on windows
  • verify that we never get EMFILE errors
  • Ignore ENOSYS from chmod/chown
  • clarify that graceful-fs must be used as a drop-in

v2.1.0

  • Use eval rather than monkey-patching fs.
  • readdir: Always sort the results
  • win32: requeue a file if error has an OK status

v2.0

  • A return to monkey patching
  • wrap process.cwd

v1.1

  • wrap readFile
  • Wrap fs.writeFile.
  • readdir protection
  • Don't clobber the fs builtin
  • Handle fs.read EAGAIN errors by trying again
  • Expose the curOpen counter
  • No-op lchown/lchmod if not implemented
  • fs.rename patch only for win32
  • Patch fs.rename to handle AV software on Windows
  • Close #4 Chown should not fail on einval or eperm if non-root
  • Fix isaacs/fstream#1 Only wrap fs one time
  • Fix #3 Start at 1024 max files, then back off on EMFILE
  • lutimes that doens't blow up on Linux
  • A full on-rewrite using a queue instead of just swallowing the EMFILE error
  • Wrap Read/Write streams as well

1.0

  • Update engines for node 0.6
  • Be lstat-graceful on Windows
  • first