Difficulty: Beginner
How do you perform file system operations in Node.js? Show the difference between callback, sync, and promise-based approaches.
The fs module provides three APIs for file operations: callback-based (fs.readFile), synchronous (fs.readFileSync), and promise-based (fs/promises). The promise-based API is the modern recommended approach as it works cleanly with async/await.
Common operations include reading files, writing files, appending data, creating directories, listing directory contents, checking if files exist, and watching for file changes. Each has callback, sync, and promise variants.
The synchronous API blocks the event loop and should only be used during startup or in CLI scripts. The callback API is the original Node.js pattern. The promise API (fs/promises) introduced in Node 10+ is now the preferred approach for application code.
Always handle errors when working with the file system. Files may not exist, permissions may be wrong, or the disk may be full. Use try/catch with async/await or .catch() with promises.
const fs = require('fs');
const fsPromises = require('fs/promises');
// 1. Callback (old pattern)
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Callback:', data);
});
// 2. Synchronous (blocks event loop!)
try {
const data = fs.readFileSync('data.txt', 'utf8');
console.log('Sync:', data);
} catch (err) {
console.error('Error:', err.message);
}
// 3. Promise-based (recommended)
async function readData() {
try {
const data = await fsPromises.readFile('data.txt', 'utf8');
console.log('Async:', data);
} catch (err) {
console.error('Error:', err.message);
}
}
readData();
Promise-based fs is cleanest with async/await. Sync blocks the loop. Callbacks lead to nesting. Always specify encoding for text files.
const fs = require('fs/promises');
const path = require('path');
async function fileOperations() {
const dir = path.join(__dirname, 'output');
// Create directory (recursive handles nested paths)
await fs.mkdir(dir, { recursive: true });
// Write file (creates or overwrites)
await fs.writeFile(
path.join(dir, 'report.json'),
JSON.stringify({ status: 'ok', count: 42 }, null, 2)
);
// Append to file
await fs.appendFile(
path.join(dir, 'log.txt'),
`[${new Date().toISOString()}] Server started\n`
);
// List directory contents
const files = await fs.readdir(dir);
console.log('Files:', files);
// Get file info
const stats = await fs.stat(path.join(dir, 'report.json'));
console.log('Size:', stats.size, 'bytes');
console.log('Is file:', stats.isFile());
}
fileOperations();
mkdir with recursive: true is like mkdir -p. writeFile overwrites, appendFile adds to end. stat gives file metadata.
const fs = require('fs/promises');
const { constants } = require('fs');
async function safeFileCopy(src, dest) {
// Check if source exists and is readable
try {
await fs.access(src, constants.R_OK);
} catch {
throw new Error(`Source file not readable: ${src}`);
}
// Check if destination already exists
try {
await fs.access(dest);
console.log('Destination exists, will overwrite');
} catch {
// File doesn't exist - that's fine
}
// Copy the file
await fs.copyFile(src, dest);
console.log(`Copied ${src} -> ${dest}`);
// Rename/move a file
await fs.rename(dest, dest + '.bak');
// Delete a file
await fs.unlink(dest + '.bak');
}
safeFileCopy('input.txt', 'output.txt');
Use fs.access() to check permissions, not fs.exists() (deprecated). copyFile, rename, and unlink are the core file manipulation operations.
fs module, Promises API, readFile, writeFile, directory operations