NodeJSThe File System (fs) Module

The File System (fs) Module

node:fs is how Node reads, writes, and manages files and directories. It's one of the oldest core modules, and it exposes the same operations in three distinct flavors — promises, callbacks, and synchronous — so the first thing to learn isn't a function, it's which API to reach for. Pick wrong and you'll either block your server or drown in callbacks.

Three APIs, one set of operations

API

Import

Shape

Use for

Promises

node:fs/promises

await readFile(...)

Default — modern async code

Callback

node:fs

readFile(..., cb)

Legacy, or perf-critical streaming callbacks

Sync

node:fs

readFileSync(...)

Scripts & startup only — it blocks

JS
// 1. Promises — preferred
import { readFile } from 'node:fs/promises'
const text = await readFile('a.txt', 'utf8')

// 2. Callback
import { readFile as readFileCb } from 'node:fs'
readFileCb('a.txt', 'utf8', (err, text) => { /* … */ })

// 3. Sync — blocks the event loop
import { readFileSync } from 'node:fs'
const text2 = readFileSync('a.txt', 'utf8')
Default to `node:fs/promises`
For almost all application code, import from `node:fs/promises` and use `await`. It's non-blocking, reads top-to-bottom, and integrates with `try/catch`. The full trade-offs of the three styles are in [Sync vs Async vs Promises API](/nodejs/fs-sync-async).
The common operations

Task

Function

Page

Read a file

readFile

Write / overwrite

writeFile

Append

appendFile

Delete / rename

unlink / rename

Directories

mkdir / readdir / rm

Metadata

stat / access

Watch for changes

watch

Text vs binary

Pass an encoding (like 'utf8') and fs gives you a string. Omit it and you get a Buffer — raw bytes — which is what you want for images, archives, or any non-text data:

JS
import { readFile } from 'node:fs/promises'

const text = await readFile('notes.txt', 'utf8')   // → string
const bytes = await readFile('photo.jpg')           // → Buffer (no encoding)
console.log(bytes.length, 'bytes')
Buffers represent binary data
When you don't specify an encoding, `fs` returns a `Buffer` — Node's fixed-length byte container. Understanding Buffers matters for any binary work; they have their own page: [Buffers & Binary Data](/nodejs/buffers).
Always handle errors
File operations fail constantly — plan for it
Missing files (`ENOENT`), permission denied (`EACCES`), not-a-directory (`ENOTDIR`), too many open files (`EMFILE`) — filesystem errors are normal, not exceptional. Every `fs` call needs error handling: `try/catch` around `await`, the error-first callback, or a `.catch`. An unhandled `fs` rejection can crash the process.

JS
import { readFile } from 'node:fs/promises'

try {
  const text = await readFile('maybe-missing.txt', 'utf8')
  console.log(text)
} catch (err) {
  if (err.code === 'ENOENT') console.error('File not found')
  else throw err                          // re-throw the unexpected
}
Anchor paths to the module
Relative paths resolve against `process.cwd()`, not your file
`readFile('data.json')` looks in the directory `node` was *launched* from, not where your script lives — so it breaks when run from elsewhere. Build paths from `import.meta.dirname` (ESM) or `__dirname` (CJS) using `node:path`. See [The path Module](/nodejs/path-module) and [Working with File Paths](/nodejs/file-paths).
For large files, think streams
`readFile` loads the WHOLE file into memory
`readFile`/`writeFile` buffer the entire file at once — fine for configs and small assets, disastrous for a 2 GB log (you'll exhaust memory). For large files, use **streams** (`createReadStream`/`createWriteStream`), which process data in chunks. The Streams section covers this: [Introduction to Streams](/nodejs/streams-intro).
Next
Start with the most common operation, in all its forms: [Reading Files](/nodejs/reading-files).