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.
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 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 formkdirpin most cases.
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
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
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
You need to delete the dist folder before every build run.
rimrafconst rimraf = require('rimraf');
rimraf.sync('./dist');
You are creating a CLI that generates a folder structure with config files.
fs-extraoutputFile to write configs into nested folders in one step.const fse = require('fs-extra');
await fse.outputFile('./src/config/db.json', configData);
Your app processes thousands of files simultaneously and hits EMFILE errors.
graceful-fsconst fs = require('graceful-fs');
// Import this at the very top of your entry file
You must support old Node versions that lack fs.mkdir recursive option.
mkdirpconst mkdirp = require('mkdirp');
await mkdirp('./deep/nested/path');
Modern Node.js versions (v14+) have closed the gap significantly.
fs.mkdir(path, { recursive: true }) instead of mkdirp.fs.rm(path, { recursive: true, force: true }) instead of rimraf.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.
| Package | Primary Use | Native Alternative | Active Maintenance |
|---|---|---|---|
fs-extra | Extended ops (copy, move, output) | Partial (fs.promises) | ✅ Yes |
graceful-fs | Stability (EMFILE, locking) | No (patches fs) | ✅ Yes |
mkdirp | Recursive directory creation | fs.mkdir (v10.12+) | ✅ Yes |
node-fs | Legacy promise/shim | fs.promises | ❌ No (Obsolete) |
rimraf | Recursive deletion | fs.rm (v14.14+) | ✅ Yes |
Think in terms of necessity and runtime version:
fs-extra.mkdirp and rimraf.fs for mkdir/rm, but keep fs-extra for convenience methods.graceful-fs and require it at your app entry point.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.
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.
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+.
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.
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.
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.
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.
open and readdir calls, and retries them once
something closes if there is an EMFILE error from too many file
descriptors.lchmod for Node versions prior to 0.6.2.fs.lutimes if possible. Otherwise it becomes a noop.EINVAL and EPERM errors in chown, fchown or
lchown if the user isn't root.lchmod and lchown become noops, if not available.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.
// 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.
})
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.
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.
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.