Skip to main content

File System Module (fs)

The fs module is one of the most useful built-in modules in Node.js. It allows you to work with the file system on your computer: reading files, creating files, updating files, deleting files, and renaming files. To use it, you must first require it:

Synchronous vs Asynchronous

Most methods in the fs module have both synchronous and asynchronous versions.
  • Asynchronous methods take a callback function as the last argument. They are non-blocking.
  • Synchronous methods block the execution until the operation completes. They usually end with Sync.
Why this matters: Imagine your server is a single checkout lane at a grocery store (the event loop). A synchronous file read is like one customer paying with a bag of pennies—every customer behind them waits. An asynchronous read is like taking the customer’s number and calling them back when the transaction clears, so the line keeps moving. Recommendation: Always use asynchronous methods in production to avoid blocking the event loop. Synchronous methods are acceptable only during startup (reading config files before the server begins accepting requests) or in CLI scripts where you are the only user.

fs API Style Comparison

Node.js offers three ways to do file operations. Here is how they compare: Decision framework: Use fs/promises by default in any new code. Use synchronous methods only in two situations: (1) reading config at process startup before the server starts accepting connections, or (2) CLI scripts where blocking is acceptable. Use callback-style only when integrating with legacy callback-based code that you cannot refactor.

Reading Files

Asynchronous Read

Note: If you don’t specify the encoding (‘utf8’), you will get a raw Buffer object instead of a string. This is a common source of confusion—you try to log the contents and see <Buffer 48 65 6c 6c 6f> instead of readable text.

Synchronous Read

Writing Files

Asynchronous Write

fs.writeFile() replaces the file and content if it exists. If the file doesn’t exist, a new file, containing the specified content, will be created.

Appending to Files

To add content to the end of a file without replacing it, use fs.appendFile().

Directories

Creating a Directory

Reading a Directory

Removing Files and Directories

Promise-based API (fs/promises)

Modern Node.js provides a promise-based API that works beautifully with async/await.
Always use fs/promises with async/await for cleaner, more maintainable code in modern Node.js applications.

Watching Files for Changes

Working with JSON Files

Practical Example: File-based Logger

Performance: readFile vs Streams vs readline

Choosing the right file-reading approach depends on file size and what you need to do with the data: Benchmark intuition (rough numbers on modern hardware):
  • readFileSync on a 1MB file: ~2ms
  • readFileSync on a 100MB file: ~80ms (and blocks the event loop the entire time)
  • createReadStream on a 100MB file: ~80ms total, but the event loop stays responsive throughout
  • readFileSync on a 1GB file: ~800ms of blocked event loop — every request to your server waits
Edge case — reading the same file concurrently: If 100 requests all call readFile on the same 10MB file simultaneously, you will use ~1GB of RAM (100 copies of the file). Node.js does not deduplicate concurrent reads of the same file. For frequently-accessed files, read once at startup and cache the result, or use a stream that pipes directly to the response.

Common Pitfalls with File Operations

Race conditions with fs.existsSync: A common anti-pattern is checking if a file exists and then operating on it. Between your check and your operation, another process could delete or modify the file. Instead, just perform the operation and handle the error:

Common fs Error Codes

When file operations fail, the error object includes a code property. Knowing these codes lets you handle failures precisely: Edge case — EMFILE in production: The default file descriptor limit on many Linux systems is 1024. A busy server that opens files for logging, uploads, and static assets can hit this limit. Symptoms: random “EMFILE: too many open files” errors that seem intermittent because they depend on how many files are open at that moment. Fix: increase the limit with ulimit -n 65536 in your startup script, and ensure you always close file handles (streams auto-close on end/error, but manual fs.open calls need explicit fs.close).

Summary

  • Use asynchronous methods in production (avoid Sync methods except at startup)
  • Prefer fs/promises with async/await for modern code—it eliminates callback nesting
  • Use fs.watch() to monitor file/directory changes (but note it is platform-dependent and can fire duplicate events)
  • fs.stat() provides file metadata (size, dates, type)
  • Always handle errors—file not found (ENOENT), permission denied (EACCES), and disk full (ENOSPC) are the most common
  • For large files, use Streams instead of readFile (covered in detail in Chapter 06)
  • Use { recursive: true } with fs.mkdir to safely create nested directories without checking existence first