Skip to content

Node.js File System (fs) Module

The fs (file system) module is one of the most-used built-in modules in Node.js, powering everything from config loading to log writing. This lesson covers reading, writing, and managing files and directories.

What Is the fs Module?

fs gives JavaScript direct access to the file system, something browsers deliberately don't allow for security reasons. You can read and write files, create and remove directories, check file metadata, and watch for changes, all through one built-in module.

fs exposes three parallel API styles for most operations: synchronous (fs.readFileSync, blocks the event loop until done), callback-based (fs.readFile(path, cb), the original async style), and promise-based (fs.promises.readFile or node:fs/promises, designed for use with async/await).

import { readFile, writeFile } from 'node:fs/promises';

async function main() {
  const data = await readFile('./notes.txt', 'utf8');
  console.log('Current notes:', data);

  await writeFile('./notes.txt', data + '\nNew line added.');
  console.log('File updated.');
}

main();

The promise-based API (node:fs/promises) is the recommended default for new code, it pairs naturally with async/await and avoids callback nesting.

The Three fs API Styles

fs.readFileSync(path, 'utf8')                 // sync, blocks
fs.readFile(path, 'utf8', (err, data) => {})    // callback, async
await fs.promises.readFile(path, 'utf8')        // promise, async
  • Sync methods end in Sync and block the whole event loop until they finish.
  • Callback methods are the original async style, error-first callback (err, data).
  • Promise methods live under fs.promises (or import from node:fs/promises) and work with await.
  • Prefer promise-based or callback-based methods for anything running inside a server.

fs Module Cheatsheet

The file system operations you'll reach for constantly.

Task Promise API
Read a file await readFile(path, 'utf8')
Write a file await writeFile(path, data)
Append to a file await appendFile(path, data)
Delete a file await unlink(path)
Create a directory await mkdir(path, { recursive: true })
List a directory await readdir(path)
Check existence await access(path) (throws if missing)
Get file info await stat(path)

Sync vs Async: Why It Matters

fs.readFileSync() blocks the entire event loop while it runs, no other request, timer, or callback can execute in that Node.js process until the read finishes. That's usually fine for one-off scripts or reading config at startup, but disastrous inside a running web server, where it would freeze every connected client.

// OK at startup, config read once before the server starts
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync('./config.json', 'utf8'));

// Avoid inside a request handler, it blocks every other request
app.get('/report', (req, res) => {
  const data = readFileSync('./huge-report.csv', 'utf8'); // blocks!
  res.send(data);
});

Working With Directories

Beyond individual files, fs also manages directories: creating nested folder structures, listing contents, and getting detailed metadata about whether a path is a file or a directory.

import { mkdir, readdir, stat } from 'node:fs/promises';

await mkdir('./uploads/2026/07', { recursive: true });

const entries = await readdir('./uploads');
for (const entry of entries) {
  const info = await stat(`./uploads/${entry}`);
  console.log(entry, info.isDirectory() ? 'directory' : 'file');
}

{ recursive: true } on mkdir creates every missing intermediate folder, similar to mkdir -p on the command line.

Common Mistakes

  • Using fs.readFileSync() inside route handlers of a running HTTP server, blocking all other requests.
  • Forgetting the 'utf8' encoding argument, which causes readFile to return a raw Buffer instead of a string.
  • Not handling ENOENT errors when a file might not exist, and letting the process crash unhandled.
  • Building file paths with string concatenation instead of the path module, breaking cross-platform compatibility.

Key Takeaways

  • The fs module provides sync, callback, and promise-based APIs for the same operations.
  • Sync methods block the entire event loop and should be avoided in running servers.
  • node:fs/promises pairs naturally with async/await and is the recommended default.
  • { recursive: true } on mkdir creates nested directories in one call.

Pro Tip

Wrap file operations that might fail (missing files, permission errors) in try/catch and check error.code (like 'ENOENT') to handle specific failure cases gracefully instead of letting your process crash.