Essential Reference

Mastering the
Node.js File System

A comprehensive guide to reading, writing, and manipulating files with elegance and precision. From basic operations to advanced streaming techniques.

Async/Await
Streams
Error Handling

Three Ways to Work with Files

⏸️

Synchronous

Blocks execution until complete. Simple but halts everything.

readFileSync()
πŸ“ž

Callbacks

Non-blocking with traditional Node.js callback pattern.

readFile(cb)
Recommended
✨

Promises

Clean async/await syntax. Modern and readable.

fs/promises
01

Reading Files

The file system module provides multiple approaches to reading data, each suited to different scenarios. Whether you need to load a small configuration file or process a massive dataset, choosing the right method ensures both performance and reliability.
Synchronous

Blocking Read

Useful for configuration files during application startup when you need data before proceeding.

const fs = require('fs');

try {
    const data = fs.readFileSync('config.txt', 'utf8');
    console.log(data);
} catch (error) {
    console.error('Failed to read:', error.message);
}
⚠️

Avoid in web servers or during request handling. Blocks the entire event loop until complete.

Recommended

Asynchronous with Promises

The modern approach. Non-blocking with clean error handling using async/await syntax.

const fs = require('fs').promises;

async function readDocument(path) {
    try {
        const data = await fs.readFile(path, 'utf8');
        return data;
    } catch (error) {
        if (error.code === 'ENOENT') {
            console.error('File not found');
        } else {
            console.error('Read failed:', error);
        }
        throw error;
    }
}

🎯 Pattern: Reading JSON Files

async function readJSON(filename) {
    const data = await fs.readFile(filename, 'utf8');
    return JSON.parse(data);
}

// Usage
const config = await readJSON('config.json');
02

Writing & Modifying

writeFile()

Creates new file or replaces existing content entirely.

await fs.writeFile('file.txt', 'content')

appendFile()

Adds content to end of file. Creates file if doesn't exist.

await fs.appendFile('log.txt', 'entry')

copyFile()

Copies file from source to destination.

await fs.copyFile('src.txt', 'dest.txt')

rename()

Moves or renames files and directories.

await fs.rename('old.txt', 'new.txt')

Practical Example: Logger

async function log(message) {
    const timestamp = new Date().toISOString();
    const entry = `[${timestamp}] ${message}\n`;
    await fs.appendFile('app.log', entry, 'utf8');
}

// Usage
await log('Application started');
await log('User logged in');
03

Directory Operations

Creating Nested Structures

// Create single directory
await fs.mkdir('new-folder');

// Create nested directories (recursive)
await fs.mkdir('path/to/nested/folder', { recursive: true });

Reading Directory Contents

// Simple list
const files = await fs.readdir('./my-folder');
console.log(files); // ['file1.txt', 'file2.js']

// With file type information
const entries = await fs.readdir('./my-folder', { withFileTypes: true });
entries.forEach(entry => {
    console.log(entry.name, entry.isDirectory() ? 'πŸ“' : 'πŸ“„');
});

Recursive Listing Pattern

async function listFilesRecursively(dir, files = []) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            await listFilesRecursively(fullPath, files);
        } else {
            files.push(fullPath);
        }
    }
    return files;
}
04

Working with Streams

For large files, streams are essential. They process data in chunks, keeping memory usage constant regardless of file size.

Readable Streams

const readStream = fs.createReadStream('large-file.txt');

readStream.on('data', chunk => {
    console.log('Chunk:', chunk.length);
});

readStream.on('end', () => {
    console.log('Complete');
});

Piping Streams

const read = fs.createReadStream('source.txt');
const write = fs.createWriteStream('dest.txt');

read.pipe(write);

write.on('finish', () => {
    console.log('Copied!');
});

When to Use Streams

  • βœ“ Files larger than 100MB
  • βœ“ Processing data on the fly
  • βœ“ Network responses
  • βœ“ Real-time data processing
05

File Watching

// Watch a single file
fs.watch('config.txt', (eventType, filename) => {
    console.log(`${filename} changed: ${eventType}`);
});

// Watch directory recursively
fs.watch('./src', { recursive: true }, (eventType, filename) => {
    console.log(`${filename} was ${eventType}`);
});

// Detailed change info
fs.watchFile('important.txt', (current, previous) => {
    console.log(`Modified: ${current.mtime}`);
});
06

Advanced Operations

File Descriptors

Low-level control for precise read/write operations.

const handle = await fs.open('data.txt', 'r');

try {
    const buffer = Buffer.alloc(100);
    const result = await handle.read(buffer, 0, 100, 0);
    console.log(buffer.toString('utf8', 0, result.bytesRead));
} finally {
    await handle.close(); // Always close!
}

File Permissions

// Make executable
await fs.chmod('script.sh', 0o755);

// Private file
await fs.chmod('secret.txt', 0o600);
0o755 rwxr-xr-x
0o644 rw-r--r--
0o600 rw-------
07

Error Handling

Common Error Codes

ENOENT

File or directory does not exist

EACCES

Permission denied

EISDIR

Expected file, found directory

EEXIST

File already exists

Robust Error Handling Pattern

async function safeOperation(filepath) {
    try {
        return await fs.readFile(filepath, 'utf8');
    } catch (error) {
        switch (error.code) {
            case 'ENOENT':
                throw new Error(`File not found: ${filepath}`);
            case 'EACCES':
                throw new Error(`Permission denied: ${filepath}`);
            case 'EISDIR':
                throw new Error(`Expected file, got directory: ${filepath}`);
            default:
                throw new Error(`Unexpected error: ${error.message}`);
        }
    }
}
08

Best Practices

Scenario Approach Reason
Application startup config readFileSync Need config before accepting requests
Web server requests fs/promises Don't block concurrent requests
Files > 100MB createReadStream Constant memory usage
One-time scripts Sync methods Simplicity over performance

Always Close Handles

Use try/finally blocks to ensure file descriptors are closed, preventing memory leaks.

Validate Paths

Never use user input directly in file paths. Use path.join() and validation.

Quick Reference

fs.readFile()
Read entire file
fs.writeFile()
Write/overwrite file
fs.appendFile()
Add to existing file
fs.mkdir()
Create directory
fs.readdir()
List directory contents
fs.stat()
Get file metadata