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 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 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 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.
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 adds file system methods that aren't included in the native fs module and adds promise support to the fs methods. It also uses graceful-fs to prevent EMFILE errors. It should be a drop in replacement for fs.
I got tired of including mkdirp, rimraf, and ncp in most of my projects.
npm install fs-extra
fs-extra is a drop in replacement for native fs. All methods in fs are attached to fs-extra. All fs methods return promises if the callback isn't passed.
You don't ever need to include the original fs module again:
const fs = require('fs') // this is no longer necessary
you can now do this:
const fs = require('fs-extra')
or if you prefer to make it clear that you're using fs-extra and not fs, you may want
to name your fs variable fse like so:
const fse = require('fs-extra')
you can also keep both, but it's redundant:
const fs = require('fs')
const fse = require('fs-extra')
NOTE: The deprecated constants fs.F_OK, fs.R_OK, fs.W_OK, & fs.X_OK are not exported on Node.js v24.0.0+; please use their fs.constants equivalents.
There is also an fs-extra/esm import, that supports both default and named exports. However, note that fs methods are not included in fs-extra/esm; you still need to import fs and/or fs/promises seperately:
import { readFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { outputFile, outputFileSync } from 'fs-extra/esm'
Default exports are supported:
import fs from 'fs'
import fse from 'fs-extra/esm'
// fse.readFileSync is not a function; must use fs.readFileSync
but you probably want to just use regular fs-extra instead of fs-extra/esm for default exports:
import fs from 'fs-extra'
// both fs and fs-extra methods are defined
Most methods are async by default. All async methods will return a promise if the callback isn't passed.
Sync methods on the other hand will throw if an error occurs.
Also Async/Await will throw an error if one occurs.
Example:
const fs = require('fs-extra')
// Async with promises:
fs.copy('/tmp/myfile', '/tmp/mynewfile')
.then(() => console.log('success!'))
.catch(err => console.error(err))
// Async with callbacks:
fs.copy('/tmp/myfile', '/tmp/mynewfile', err => {
if (err) return console.error(err)
console.log('success!')
})
// Sync:
try {
fs.copySync('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
// Async/Await:
async function copyFiles () {
try {
await fs.copy('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
}
copyFiles()
NOTE: You can still use the native Node.js methods. They are promisified and copied over to fs-extra. See notes on fs.read(), fs.write(), & fs.writev()
walk() and walkSync()?They were removed from fs-extra in v2.0.0. If you need the functionality, walk and walkSync are available as separate packages, klaw and klaw-sync.
fse-cli allows you to run fs-extra from a console or from npm scripts.
If you like TypeScript, you can use fs-extra with it: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra
If you want to watch for changes to files or directories, then you should use chokidar.
fs-filesystem allows you to read the state of the filesystem of the host on which it is run. It returns information about both the devices and the partitions (volumes) of the system.
Wanna hack on fs-extra? Great! Your help is needed! fs-extra is one of the most depended upon Node.js packages. This project
uses JavaScript Standard Style - if the name or style choices bother you,
you're gonna have to get over it :) If standard is good enough for npm, it's good enough for fs-extra.
What's needed?
Note: If you make any big changes, you should definitely file an issue for discussion first.
fs-extra contains hundreds of tests.
npm run lint: runs the linter (standard)npm run unit: runs the unit testsnpm run unit-esm: runs tests for fs-extra/esm exportsnpm test: runs the linter and all testsWhen running unit tests, set the environment variable CROSS_DEVICE_PATH to the absolute path of an empty directory on another device (like a thumb drive) to enable cross-device move tests.
If you run the tests on the Windows and receive a lot of symbolic link EPERM permission errors, it's
because on Windows you need elevated privilege to create symbolic links. You can add this to your Windows's
account by following the instructions here: http://superuser.com/questions/104845/permission-to-make-symbolic-links-in-windows-7
However, I didn't have much luck doing this.
Since I develop on Mac OS X, I use VMWare Fusion for Windows testing. I create a shared folder that I map to a drive on Windows.
I open the Node.js command prompt and run as Administrator. I then map the network drive running the following command:
net use z: "\\vmware-host\Shared Folders"
I can then navigate to my fs-extra directory and run the tests.
I put a lot of thought into the naming of these functions. Inspired by @coolaj86's request. So he deserves much of the credit for raising the issue. See discussion(s) here:
First, I believe that in as many cases as possible, the Node.js naming schemes should be chosen. However, there are problems with the Node.js own naming schemes.
For example, fs.readFile() and fs.readdir(): the F is capitalized in File and the d is not capitalized in dir. Perhaps a bit pedantic, but they should still be consistent. Also, Node.js has chosen a lot of POSIX naming schemes, which I believe is great. See: fs.mkdir(), fs.rmdir(), fs.chown(), etc.
We have a dilemma though. How do you consistently name methods that perform the following POSIX commands: cp, cp -r, mkdir -p, and rm -rf?
My perspective: when in doubt, err on the side of simplicity. A directory is just a hierarchical grouping of directories and files. Consider that for a moment. So when you want to copy it or remove it, in most cases you'll want to copy or remove all of its contents. When you want to create a directory, if the directory that it's suppose to be contained in does not exist, then in most cases you'll want to create that too.
So, if you want to remove a file or a directory regardless of whether it has contents, just call fs.remove(path). If you want to copy a file or a directory whether it has contents, just call fs.copy(source, destination). If you want to create a directory regardless of whether its parent directories exist, just call fs.mkdirs(path) or fs.mkdirp(path).
fs-extra wouldn't be possible without using the modules from the following authors:
Licensed under MIT
Copyright (c) 2011-2024 JP Richardson