The fs module, sync vs async variants, path.join and path.resolve, process.env and process.argv — reading from and writing to the real world.
Module F-3 — Working with Files, Paths, and the Environment
What this module covers: The moment you write a server, a script, or a CLI tool, you need to read files, write logs, parse paths, and pull configuration from the environment. This module covers Node.js's
fsmodule for file I/O, thepathmodule for platform-safe path manipulation, and theprocessobject for environment variables and command-line arguments. These three are in practically every Node.js program you will ever write.
The fs Module
The fs (file system) module is how Node.js reads and writes files on disk. Every operation comes in two flavours: synchronous (blocks until complete) and asynchronous (non-blocking, uses a callback or Promise). We'll cover when to use each.
Reading a File
Async with callback (old style — you will see this in legacy code):
const fs = require('node:fs');
fs.readFile('./data.txt', 'utf8', (err, contents) => {
if (err) {
console.error('Failed to read file:', err.message);
return;
}
console.log(contents);
});
console.log('This runs BEFORE the file contents are logged');
The callback receives two arguments by convention: err first, then the result. If err is not null, something went wrong. This error-first callback pattern is a Node.js convention you will see everywhere in older code.
A gotcha worth flagging early: in the example above, the second argument to fs.readFile() is 'utf8' — the encoding. If you leave that argument out, Node.js does not give you a string at all; it gives you a raw Buffer (a chunk of binary data). Forgetting the encoding is one of the most common beginner mistakes with fs — you console.log() what you expect to be text and instead see something like <Buffer 68 65 6c 6c 6f>. Always pass 'utf8' explicitly when you want a string back.
A limit worth knowing: fs.readFile() (in any of its forms — callback, Promise, or sync) reads the entire file into memory before your callback runs or your await resolves. For a config file or a small log, that's fine. For a multi-gigabyte file, it isn't — you'll either exhaust memory or wait far longer than necessary before processing the first byte. When files get large, the right tool is a stream, which processes data in chunks as it arrives instead of loading it all at once. Streams are covered in depth in Phase 2.
Async with Promises (modern style — use this):
const fs = require('node:fs/promises');
async function readConfig() {
try {
const contents = await fs.readFile('./config.json', 'utf8');
const config = JSON.parse(contents);
console.log(config);
} catch (err) {
console.error('Failed to read config:', err.message);
}
}
readConfig();
node:fs/promises is the Promise-based version of the fs module, available since Node.js 14. Always prefer this in new code — it works with async/await and avoids callback nesting.
Synchronous (avoid in servers, fine in scripts):
const fs = require('node:fs');
try {
const contents = fs.readFileSync('./data.txt', 'utf8');
console.log(contents);
} catch (err) {
console.error('Failed:', err.message);
}
readFileSync blocks the entire Node.js process until the file is read. In a script that runs once and exits, this is fine. In a server handling concurrent requests, never use it — you will block every other request while one request waits for a file read.
Writing a File
const fs = require('node:fs/promises');
async function writeLog(message) {
await fs.writeFile('./app.log', message + '\n', { flag: 'a' });
// flag: 'a' = append (don't overwrite)
// flag: 'w' = overwrite (default)
}
await writeLog('Server started at ' + new Date().toISOString());
Common flags:
'w'— write (creates or overwrites)'a'— append (creates or adds to end)'wx'— write, but fail if file already exists (useful for "create once" files)
Checking if a File Exists
const fs = require('node:fs/promises');
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
if (await fileExists('./config.json')) {
console.log('Config found');
} else {
console.log('No config — using defaults');
}
Avoid the old fs.existsSync() pattern. It creates a race condition: by the time you act on the result (open the file), another process may have deleted it. The correct pattern is to attempt the operation and handle the error — exactly what try/catch around fs.access does.
Creating Directories
const fs = require('node:fs/promises');
// Create a single directory
await fs.mkdir('./logs');
// Create nested directories (like mkdir -p)
await fs.mkdir('./data/uploads/images', { recursive: true });
// recursive: true won't throw if the directory already exists
Listing Directory Contents
const fs = require('node:fs/promises');
const entries = await fs.readdir('./src');
console.log(entries);
// [ 'app.js', 'config.js', 'routes', 'utils' ]
// Get full details (type, size, etc.)
const detailed = await fs.readdir('./src', { withFileTypes: true });
for (const entry of detailed) {
const type = entry.isDirectory() ? 'dir' : 'file';
console.log(`${type}: ${entry.name}`);
}
Deleting Files and Directories
const fs = require('node:fs/promises');
// Delete a file
await fs.unlink('./temp.txt');
// Delete a directory and all its contents (Node 14.14+)
await fs.rm('./old-uploads', { recursive: true, force: true });
// force: true won't throw if the path doesn't exist
Renaming and Moving Files
const fs = require('node:fs/promises');
// Rename (works as move within the same filesystem)
await fs.rename('./draft.txt', './published.txt');
// Move to a different directory
await fs.rename('./uploads/temp_abc123.jpg', './uploads/processed/final.jpg');
Getting File Metadata
const fs = require('node:fs/promises');
const stats = await fs.stat('./package.json');
console.log('Size:', stats.size, 'bytes');
console.log('Is file:', stats.isFile());
console.log('Is directory:', stats.isDirectory());
console.log('Last modified:', stats.mtime);
console.log('Created:', stats.birthtime);
The path Module
File paths are one of the most common sources of bugs in cross-platform code. On Windows, paths use backslashes (C:\Users\jatin). On macOS and Linux, they use forward slashes (/home/jatin). The path module abstracts this so your code works everywhere.
const path = require('node:path');Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion