fs vs fs-extra vs graceful-fs vs memfs
File System Abstractions in Node.js: Native, Enhanced, Resilient, and Virtual
fsfs-extragraceful-fsmemfsSimilar Packages:

File System Abstractions in Node.js: Native, Enhanced, Resilient, and Virtual

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
fs0163-510 years agoISC
fs-extra09,59059.3 kB13a month agoMIT
graceful-fs01,30232.5 kB493 years agoISC
memfs02,09169.7 kB507 hours agoApache-2.0

File System Abstractions in Node.js: Native, Enhanced, Resilient, and Virtual

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.

๐Ÿ› ๏ธ Core Functionality: Native vs. Enhanced vs. Virtual

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'

๐Ÿš€ Handling Asynchronous Operations

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 and Isolation Strategies

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
});

โš ๏ธ Error Handling and Reliability

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');
}

๐ŸŒ Environment Compatibility

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');

๐Ÿค Shared Ground: The Standard API

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.

1. ๐Ÿ“‚ Path Handling

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);

2. ๐Ÿ“ Encoding Support

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' });

3. ๐Ÿ“Š Stats and Metadata

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

๐Ÿ“Š Summary: Key Differences

Featurefsfs-extragraceful-fsmemfs
Primary GoalNative OS AccessDeveloper ConvenienceStability / Error RecoveryVirtual / 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 ForCore logic, minimal depsScripts, CLI, DXHigh-load servers, npm toolsTesting, Browser apps

๐Ÿ’ก The Big Picture

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.

How to Choose: fs vs fs-extra vs graceful-fs vs memfs

  • fs:

    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.

  • fs-extra:

    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.

  • graceful-fs:

    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.

  • memfs:

    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.

README for fs

Security holding package

This package name is not currently in use, but was formerly occupied by another package. To avoid malicious use, npm is hanging on to the package name, but loosely, and we'll probably give it to you if you want it.

You may adopt this package by contacting support@npmjs.com and requesting the name.