fs-extra
fs-extra is Node's fs module plus the methods people always ended up installing separately: recursive copy, move, remove, mkdir -p, JSON read/write, and ensure-this-path-exists helpers. Every native fs method is re-exported and promisified, so you can import fs-extra instead of fs and never think about it again. It wraps graceful-fs underneath, which retries operations when the process runs out of file descriptors. The project started because its author was tired of pulling in mkdirp, rimraf, and ncp on every project, and it became one of the most depended-upon packages on npm.
Still the pragmatic default for Node scripts and build tooling that touch the filesystem a lot, and maintenance is quiet but steady (11.4.0 shipped July 2026 with 12 open issues and PRs). If you are starting a small project on modern Node, check whether native fs.cp and fs.rm already cover you before adding it.
Use it if
- You want copy, move, remove, and emptyDir with sane recursive defaults instead of composing fs.cp, fs.rm, and fs.mkdir flags by hand
- You read and write JSON files a lot: readJson, writeJson, and outputJson (which creates parent directories) remove a whole class of boilerplate
- You support a range of Node versions and want one API that behaves the same on Node 14.14 through current, callbacks and promises both
- You hit EMFILE (too many open files) in scripts that touch thousands of files; the graceful-fs layer queues and retries for you
- You are on Node 16.7+ and only need copy/rm/mkdir: native fs.cp, fs.rm({ recursive: true, force: true }), and fs.mkdir({ recursive: true }) cover the classic use cases with zero dependencies
- You are writing a library and count dependencies: fs-extra drags in graceful-fs, jsonfile, and universalify, which is real weight for what may be one mkdir call
- You need file watching, globbing, or streaming helpers: none of that is here; you still need chokidar, fast-glob, or plain streams
- You want a modern ESM-first API: the fs-extra/esm entry exists but excludes all the native fs re-exports, so ESM users juggle two imports and the docs around this trip people up
Setup reality
npm install fs-extra and require it in place of fs; no config, no native builds, and types come from @types/fs-extra if you use TypeScript. The gotchas are around module format: import { readFile } from 'fs-extra/esm' does not include native fs methods, so ESM named imports of plain fs functions fail and you need a separate node:fs import, while a default import of the main entry works fine. On Node 24+ the deprecated fs.F_OK style constants are gone; use fs.constants. Windows symlink tests and operations may need elevated privileges.
Patterns
Copy a file or directory recursivelycopy-file-or-dir
const fs = require('fs-extra')
await fs.copy('/tmp/mydir', '/tmp/copy')
await fs.copy('/tmp/file.txt', '/tmp/copy/file.txt')Directories copy recursively by default and destination parent directories are created for you. Copying a dir into itself throws.
Copy while skipping some pathscopy-with-filter
const fs = require('fs-extra')
await fs.copy('src', 'dist', {
filter: (src, dest) => !src.includes('node_modules'),
overwrite: true
})filter gets raw paths, not globs; return false to skip. If you return false for a directory, nothing inside it is visited. It can be async.
Move a file or directory across devicesmove-file
const fs = require('fs-extra')
await fs.move('/tmp/somefile', '/storage/somefile', { overwrite: true })Unlike fs.rename, move falls back to copy-then-delete when source and destination are on different devices or partitions. Without overwrite it errors if the destination exists.
Delete a file or directory treeremove-recursive
const fs = require('fs-extra')
await fs.remove('/tmp/build')
// no error if the path does not existWorks like rm -rf: recursive and silent on missing paths. On modern Node it delegates to fs.rm under the hood.
Create a directory path if missingensure-dir
const fs = require('fs-extra')
await fs.ensureDir('/tmp/this/path/does/not/exist')
// aliases: fs.mkdirs, fs.mkdirpmkdir -p semantics: creates every missing parent and succeeds if the directory already exists.
Write a file, creating parent directoriesoutput-file
const fs = require('fs-extra')
await fs.outputFile('/tmp/a/b/c/file.txt', 'hello')The difference from fs.writeFile is only that missing parent directories are created first; this is the method that replaces the mkdir-then-write dance.
Read and write JSON filesread-write-json
const fs = require('fs-extra')
const pkg = await fs.readJson('./package.json')
await fs.writeJson('./out.json', { name: 'app' }, { spaces: 2 })
await fs.outputJson('./deep/dir/out.json', { ok: true }, { spaces: 2 })writeJson writes with no whitespace unless you pass spaces. outputJson also creates parent directories. readJson with { throws: false } returns null on parse errors instead of throwing.
Empty a directory without deleting itempty-dir
const fs = require('fs-extra')
await fs.emptyDir('/tmp/some/dir')
// dir now exists and is emptyDeletes contents but keeps (or creates) the directory itself; handy for build output folders where you want the path to survive.
Check whether a path existspath-exists
const fs = require('fs-extra')
if (await fs.pathExists('/etc/passwd')) {
console.log('found it')
}The promise-friendly replacement for the deprecated fs.exists. For check-then-act flows, prefer just attempting the operation and catching ENOENT to avoid races.
Ensure a file exists (touch)ensure-file
const fs = require('fs-extra')
await fs.ensureFile('/tmp/logs/app.log')Creates the file and any missing parent directories if absent; if the file exists it is left untouched, including its content.
Use fs-extra from ESMesm-import
// simplest: default import gets fs + extras
import fs from 'fs-extra'
await fs.outputFile('/tmp/x.txt', 'hi')
// named imports of extras only:
import { outputFile, pathExists } from 'fs-extra/esm'
import { readFile } from 'node:fs/promises'fs-extra/esm exports only the extra methods; native fs functions are not re-exported there, so import { readFile } from 'fs-extra/esm' is undefined. This is the top ESM confusion with this package.
Pick promise, callback, or sync stylepromises-vs-callbacks
const fs = require('fs-extra')
// promise (no callback passed)
await fs.copy('/tmp/a', '/tmp/b')
// callback
fs.copy('/tmp/a', '/tmp/b', err => { if (err) console.error(err) })
// sync
fs.copySync('/tmp/a', '/tmp/b')Every async method returns a promise when you omit the callback. Sync variants throw on error; avoid them in servers since they block the event loop.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rimraf | npm | You only need rm -rf semantics, possibly with glob support, and nothing else from the toolbox. |
| mkdirp | npm | You only need mkdir -p and cannot use native fs.mkdir with recursive: true for some reason. |
| jsonfile | npm | You only want JSON file read/write; it is the exact module fs-extra uses internally for readJson and writeJson. |
| cpy | npm | You want file copying driven by glob patterns with progress reporting rather than a path-to-path copy. |