Difficulty: Beginner
The fs (file system) module is one of Node.js's most important built-in modules, providing an API for interacting with the file system. Reading files is the most common file system operation, and Node.js offers three distinct APIs for it: the callback-based API (fs.readFile), the synchronous API (fs.readFileSync), and the promise-based API (fs/promises). Understanding when to use each is key to writing efficient Node.js code.
The callback-based fs.readFile(path, options, callback) is the original Node.js API. It reads the entire file asynchronously and calls the callback with an error (or null) and the file data when complete. If you do not specify an encoding (like 'utf8'), the callback receives a Buffer object containing raw binary data. With encoding specified, you get a string. The callback pattern follows the Node.js convention of error-first callbacks: the first argument is always the error, and the second is the result. This pattern predates Promises and is still widely used.
fs.readFileSync(path, options) is the synchronous version that blocks the event loop until the file is completely read. It returns the file content directly (no callback needed) and throws an error if something goes wrong. Use readFileSync only at application startup for reading configuration files or during build scripts where blocking is acceptable. Never use synchronous file operations in request handlers or hot paths of a server, as it blocks all other operations.
The modern and recommended approach is the promise-based API available through 'fs/promises' (or 'fs.promises'). It provides the same operations as the callback API but returns Promises, making it compatible with async/await. This is cleaner, easier to read, and easier to handle errors with try/catch. For example: 'const data = await fs.readFile("file.txt", "utf8")'. The fs/promises module was stabilized in Node.js 14 and is now the preferred way to work with the file system.
When reading files, understanding Buffers is important. A Buffer is Node.js's way of handling raw binary data. When you read a file without specifying encoding, you get a Buffer. You can convert it to a string with buffer.toString('utf8') or specify the encoding directly in the readFile options. Common encodings include 'utf8' (text files), 'ascii', 'base64' (for embedding binary data in strings), and 'hex'. For binary files like images or PDFs, work with Buffers directly rather than converting to strings.
const fs = require('fs');
const fsPromises = require('fs/promises');
// 1. Callback-based (original API)
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.log('Callback error:', err.message);
return;
}
console.log('Callback:', data);
});
// 2. Synchronous (blocks event loop)
try {
const data = fs.readFileSync('example.txt', 'utf8');
console.log('Sync:', data);
} catch (err) {
console.log('Sync error:', err.message);
}
// 3. Promise-based (recommended)
async function readWithPromise() {
try {
const data = await fsPromises.readFile('example.txt', 'utf8');
console.log('Promise:', data);
} catch (err) {
console.log('Promise error:', err.message);
}
}
readWithPromise();
All three approaches do the same thing but with different patterns. The sync version blocks execution. The callback version is non-blocking but uses nested callbacks. The promise version is non-blocking and supports clean async/await syntax. Always prefer fs/promises for new code.
const fs = require('fs');
// Without encoding: returns a Buffer
const buffer = Buffer.from('Hello, Node.js!');
console.log('Buffer:', buffer);
console.log('Buffer hex:', buffer.toString('hex'));
console.log('Buffer base64:', buffer.toString('base64'));
console.log('Buffer utf8:', buffer.toString('utf8'));
console.log('Buffer length:', buffer.length, 'bytes');
// Reading binary data (like an image) would give you a Buffer
// const imageBuffer = fs.readFileSync('photo.jpg');
// console.log('Image size:', imageBuffer.length, 'bytes');
// Comparing encoding differences
const emoji = Buffer.from('Hello 🌍');
console.log('\nString length:', 'Hello 🌍'.length);
console.log('Buffer length:', emoji.length, '(bytes, UTF-8 encoded)');
Buffers represent raw binary data. Without encoding, readFile returns a Buffer. The emoji example shows that string length (character count) differs from buffer length (byte count) because multi-byte characters like emojis take 4 bytes in UTF-8.
const fsPromises = require('fs/promises');
// Reading and parsing a JSON file
async function readJSON(filePath) {
const raw = await fsPromises.readFile(filePath, 'utf8');
return JSON.parse(raw);
}
// Reading a CSV file line by line
async function readCSV(filePath) {
const raw = await fsPromises.readFile(filePath, 'utf8');
const lines = raw.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, i) => {
obj[header.trim()] = values[i].trim();
return obj;
}, {});
});
}
// Demo with inline data
const jsonData = JSON.parse('{"name": "Node", "version": 20}');
console.log('JSON:', jsonData);
const csvText = 'name,age\nAlice,25\nBob,30';
const lines = csvText.trim().split('\n');
const headers = lines[0].split(',');
const parsed = lines.slice(1).map(line => {
const vals = line.split(',');
return { [headers[0]]: vals[0], [headers[1]]: vals[1] };
});
console.log('CSV:', parsed);
JSON files can be read as UTF-8 strings and parsed with JSON.parse(). For CSV files, split by newlines for rows and commas for columns. In production, use a library like csv-parse for handling quoted fields, escaping, and edge cases.
readFile, readFileSync, fs/promises, Encoding, Buffers