fs is the built-in Node.js module for interacting with the operating system's file system, providing low-level access to files and directories. fs-extra is a drop-in replacement that adds missing methods (like copy and move) and promise support to the native API. graceful-fs wraps the native fs module to handle edge cases like running out of file descriptors or EMFILE errors by queuing requests. memfs implements the entire fs interface in memory, allowing developers to mock the file system for testing or run virtual file systems in environments without disk access, such as browsers.
Working with files is a core part of many Node.js applications, from build tools to backend APIs. The ecosystem offers four distinct approaches: the native fs module, the enhanced fs-extra, the resilient graceful-fs, and the virtual memfs. While they all share similar method names, they solve very different problems. Let's break down how they work and when to use each one.
fs is the built-in module that comes with Node.js. It provides direct access to the operating system's file system. It supports callbacks and, in modern versions, Promises via fs/promises, but it lacks some high-level convenience methods found in other libraries.
// fs: Native implementation
const fs = require('fs');
const { readFile } = require('fs/promises');
// Reading a file with Promises
const data = await readFile('./config.json', 'utf8');
// Note: fs.copy() does not exist natively in older Node versions without extra code
fs-extra acts as a drop-in replacement for fs. It adds methods that developers frequently need but aren't in the core module, such as copy, move, and ensureDir. It also automatically returns Promises if no callback is provided, making code cleaner.
// fs-extra: Enhanced implementation
const fse = require('fs-extra');
// Copying a directory recursively (one line)
await fse.copy('./src', './dist');
// Ensuring a directory exists before writing
await fse.ensureDir('./tmp/logs');
await fse.writeJson('./tmp/config.json', { debug: true });
graceful-fs does not add new features. Instead, it wraps the native fs module to fix common issues related to file descriptor limits. If your app tries to open too many files at once, graceful-fs queues the requests instead of crashing.
// graceful-fs: Resilient wrapper
const fs = require('graceful-fs');
// Usage looks identical to native fs
// But internally handles EMFILE errors by retrying
fs.readFile('./large-file.txt', (err, data) => {
if (err) throw err;
console.log('File read safely');
});
memfs creates a completely virtual file system in your computer's RAM. It implements the same API as fs, so your code doesn't need to change, but nothing is ever written to the actual disk. This is perfect for testing or running in browsers.
// memfs: Virtual in-memory implementation
const { Volume } = require('memfs');
const vol = new Volume();
// Write to virtual disk
vol.writeFileSync('/test.txt', 'hello world');
// Read from virtual disk
const content = vol.readFileSync('/test.txt', 'utf8');
console.log(content); // 'hello world'
How these libraries handle async logic varies significantly, impacting code readability.
fs traditionally relied on callbacks. While fs/promises exists now, mixing styles can be confusing. You often need to explicitly import the promise version.
// fs: Explicit promise import required
const { readFile } = require('fs/promises');
async function getConfig() {
return await readFile('./config.json', 'utf8');
}
fs-extra simplifies this by supporting promises out of the box for every method. If you don't pass a callback function, it returns a Promise automatically.
// fs-extra: Automatic Promise support
const fse = require('fs-extra');
async function setupProject() {
// No need to import a separate 'promises' module
await fse.mkdirs('./project/src');
await fse.outputJson('./project/package.json', { name: 'app' });
}
graceful-fs mirrors the API of whatever version of fs it wraps. It focuses on reliability rather than changing the async style. It handles the retry logic internally when errors occur.
// graceful-fs: Callback style with internal retry logic
const fs = require('graceful-fs');
fs.readFile('./data.txt', 'utf8', (err, data) => {
// If EMFILE occurs, this callback waits and retries automatically
if (err) return console.error(err);
console.log(data);
});
memfs supports both callbacks and promises, just like the native module. However, because it runs in memory, operations are nearly instant, which makes tests run much faster.
// memfs: Promise support available
const { createFsFromVolume, Volume } = require('memfs');
const vol = new Volume();
const fs = createFsFromVolume(vol);
async function testWrite() {
await fs.promises.writeFile('/log.txt', 'started');
const content = await fs.promises.readFile('/log.txt', 'utf8');
return content;
}
Testing file system code is notoriously hard because it leaves mess on your disk and depends on your OS. This is where memfs shines compared to the others.
fs, fs-extra, and graceful-fs all touch the real disk. To test them safely, you must create temporary directories and clean them up after every test. If a test crashes, you might leave junk files behind.
// fs / fs-extra: Requires manual cleanup in tests
const fs = require('fs-extra');
const tmpDir = './tmp-test-' + Date.now();
beforeEach(async () => {
await fs.ensureDir(tmpDir);
});
afterEach(async () => {
// Must remember to delete, or disk fills up
await fs.remove(tmpDir);
});
test('writes file', async () => {
await fs.writeFile(`${tmpDir}/file.txt`, 'data');
// ... assertions
});
memfs requires no cleanup. Since the file system exists only in RAM and is created fresh for every test run, there is no risk of leftover files. It also allows you to simulate complex directory structures instantly.
// memfs: No cleanup needed, completely isolated
const { Volume } = require('memfs');
const vol = new Volume.fromJSON({
'/etc/config.json': '{ "env": "test" }',
'/var/log/app.log': 'start'
});
test('reads config', () => {
const data = vol.readFileSync('/etc/config.json', 'utf8');
expect(JSON.parse(data).env).toBe('test');
// No afterEach needed; vol is discarded after test
});
Different libraries handle failure in different ways, especially under load.
fs will throw an error immediately if you hit system limits, such as opening too many files (EMFILE). In high-concurrency apps, this can cause sudden crashes.
// fs: Fails fast on resource exhaustion
fs.open('./file.txt', 'r', (err, fd) => {
if (err && err.code === 'EMFILE') {
console.error('Too many open files! App might crash.');
}
});
graceful-fs intercepts these specific errors. Instead of failing, it puts the request in a queue and waits until a file descriptor becomes available. This makes apps much more stable under heavy load.
// graceful-fs: Queues requests on error
// If EMFILE happens, it waits automatically
fs.open('./file.txt', 'r', (err, fd) => {
// This callback only runs when the file is successfully opened
// or if a non-retryable error occurs
});
fs-extra improves error messages for common mistakes, like trying to copy a file to a destination that doesn't exist yet. It often combines with graceful-fs in production tools to get both convenience and stability.
// fs-extra: Better error context
try {
await fse.copy('./src', './non-existent-dir/sub/file');
} catch (err) {
// Provides clear message about missing directory
console.error(err.message);
}
memfs simulates errors too. You can configure it to throw specific errors (like ENOENT or EACCES) to test how your app handles failure without needing to break your actual OS permissions.
// memfs: Simulating errors for tests
const vol = new Volume();
vol.throwError = true; // Hypothetical configuration for strict testing
// Or simply don't create the file to trigger ENOENT naturally
try {
vol.readFileSync('/missing.txt');
} catch (e) {
expect(e.code).toBe('ENOENT');
}
Where your code runs matters. Not all file system libraries work everywhere.
fs, fs-extra, and graceful-fs rely on Node.js internals. They cannot run in a web browser. If you try to bundle them with Webpack for a client-side app, the build will either fail or include large polyfills that don't fully work.
// fs / fs-extra / graceful-fs: Node.js only
// This will crash in a browser console
const fs = require('fs');
memfs is designed to work in both Node.js and browsers. Since it uses JavaScript objects to store files, it runs anywhere JavaScript runs. This makes it unique for universal libraries that need file system APIs on both server and client.
// memfs: Works in Browser and Node
import { Volume } from 'memfs';
// Runs perfectly in Chrome, Firefox, or Node
const vol = new Volume();
vol.writeFileSync('/browser-file.txt', 'hello');
Despite their differences, all four libraries adhere to the Node.js fs API standards. This means methods like readFile, writeFile, stat, and exists behave consistently across them.
All libraries use standard string paths or Buffer paths. They respect relative (./) and absolute (/) paths in the same way.
// Works identically in fs, fs-extra, graceful-fs, and memfs
const path = './data/file.txt';
fs.readFileSync(path);
fse.readFileSync(path);
memfsVol.readFileSync(path);
All support encoding options (like 'utf8', 'base64') as either a string argument or an options object.
// Consistent encoding API
fs.readFile('img.png', 'base64', callback);
fse.readFile('img.png', { encoding: 'base64' });
Retrieving file metadata (size, creation time) uses the same Stats object structure across all implementations.
// All return an instance of fs.Stats (or compatible)
const stats = fs.statSync('./file.txt');
console.log(stats.isFile()); // true
| Feature | fs | fs-extra | graceful-fs | memfs |
|---|---|---|---|---|
| Primary Goal | Native OS Access | Developer Convenience | Stability / Error Recovery | Virtual / In-Memory FS |
| Extra Methods | โ No | โ
Yes (copy, move, json) | โ No | โ Yes (Volume management) |
| Promise Support | โ
Via fs/promises | โ Automatic | โ ๏ธ Depends on Node version | โ Yes |
| Disk Access | โ Real Disk | โ Real Disk | โ Real Disk | โ RAM Only |
| Browser Ready | โ No | โ No | โ No | โ Yes |
| Best For | Core logic, minimal deps | Scripts, CLI, DX | High-load servers, npm tools | Testing, Browser apps |
Choosing the right file system library depends entirely on your environment and goals.
fs is your default for standard server-side logic. It's always there, requires no installation, and is perfect for simple read/write tasks where you don't need extra helpers.
fs-extra is the pragmatic choice for tooling and complex backend logic. If you find yourself writing helper functions to copy directories or ensure paths exist, fs-extra saves you time and reduces bugs with its battle-tested utilities.
graceful-fs is the safety net. You might not need it for a small script, but for any long-running server or package manager that handles thousands of files, it prevents hard crashes due to OS limits. Many teams install it simply to patch the native fs module globally.
memfs is the specialist for testing and universal code. It solves the hardest problem in file system testing: isolation. By keeping your tests fast and clean, it pays for itself immediately in large projects. It also unlocks the ability to run file-based logic in the browser, which no other option can do.
Final Thought: In a mature Node.js architecture, you will often see these used together. A common pattern is using graceful-fs to patch the native module for stability, fs-extra for convenient scripting, and memfs to mock everything out during testing. Understanding the specific role of each allows you to build file-handling code that is robust, easy to read, and safe to run anywhere.
Select fs-extra if you want a more ergonomic developer experience with built-in Promise support and helpful utilities like ensureDir, copy, and readJson. It is ideal for build scripts, CLI tools, and backend services where reducing boilerplate code and handling common file operations safely is more important than avoiding a single extra dependency.
Choose the native fs module for production server-side applications where you need zero external dependencies and full control over the standard Node.js API. It is the best choice when your team is comfortable handling callbacks or wrapping methods in promises manually, and you do not need extra utility functions like recursive copying or JSON file handling.
Use graceful-fs specifically when your application performs heavy concurrent file operations that might hit OS limits on open file descriptors (EMFILE errors). It is often used implicitly by tools like npm or test runners, but you should explicitly add it to your project if you encounter 'too many open files' crashes in high-load scenarios.
Pick memfs when you need to unit test code that interacts with the file system without touching the actual disk, or when running Node.js code in a browser environment. It is essential for creating isolated, fast tests that verify file logic without side effects, or for bundling applications that expect a file system API but run in memory-only contexts.
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 separately:
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