Node.js File Writing Explained Simply

Writing files in Node.js is a fundamental skill that every developer needs. Whether you’re creating log files, saving user uploads, or generating reports, understanding how to write files efficiently and safely is crucial.

This guide covers everything you need to know about file writing in Node.js, from basic operations to advanced techniques.

Understanding the File System Module

Node.js provides file system operations through the built-in fs module. This module offers three different API styles:

  • Callback-based (traditional)
  • Promise-based (modern, recommended)
  • Synchronous (blocking, use sparingly)
// Different ways to import fs
import fs from 'fs';                    // Callback-based
import { promises as fsPromises } from 'fs';   // Promise-based
import { writeFile } from 'fs/promises'; // Destructured promises

Basic File Writing with fs.writeFile()

The fs.writeFile() method is the most common way to write files asynchronously.

Basic Syntax

fs.writeFile(file, data[, options], callback)

Parameters:

  • file: String or Buffer - file path
  • data: String, Buffer, or Uint8Array - content to write
  • options: Object (optional) - encoding, mode, flag
  • callback: Function - called when operation completes

Simple Example

import fs from 'fs';

fs.writeFile('message.txt', 'Hello World!', (err) => {
  if (err) {
    console.error('Error writing file:', err);
    return;
  }
  console.log('File written successfully!');
});

Writing Different Data Types

// Write a string
fs.writeFile('text.txt', 'This is a string', (err) => {
  if (err) throw err;
});

// Write a Buffer
const buffer: Buffer = Buffer.from('This is a buffer');
fs.writeFile('buffer.txt', buffer, (err) => {
  if (err) throw err;
});

// Write JSON data
const data = { name: 'John', age: 30 };
fs.writeFile('data.json', JSON.stringify(data, null, 2), (err) => {
  if (err) throw err;
});

Promise-Based File Writing

Modern Node.js applications should use the promise-based API for cleaner, more maintainable code.

Using fsPromises.writeFile()

import { promises as fsPromises } from 'fs';

async function writeFileExample() {
  try {
    await fsPromises.writeFile('example.txt', 'Hello from promises!');
    console.log('File written successfully!');
  } catch (error) {
    console.error('Error writing file:', error);
  }
}

writeFileExample();

Destructured Import

import { writeFile } from 'fs/promises';

async function writeFileExample() {
  try {
    await writeFile('example.txt', 'Hello from destructured import!');
    console.log('File written successfully!');
  } catch (error) {
    console.error('Error writing file:', error);
  }
}

Error Handling with Promises

import { writeFile } from 'fs/promises';

async function robustFileWriting(): Promise<void> {
  try {
    await writeFile('important.txt', 'Critical data');
    console.log('File saved successfully');
  } catch (error) {
    if (error instanceof Error && 'code' in error) {
      if (error.code === 'ENOSPC') {
        console.error('No space left on device');
      } else if (error.code === 'EACCES') {
        console.error('Permission denied');
      } else {
        console.error('Unexpected error:', error.message);
      }
    } else {
      console.error('Unexpected error:', error);
    }
  }
}

Synchronous File Writing

While generally not recommended, synchronous file writing can be useful in specific scenarios like CLI tools or initialization scripts.

Using fs.writeFileSync()

import fs from 'fs';

try {
  fs.writeFileSync('sync.txt', 'Written synchronously');
  console.log('File written successfully');
} catch (error) {
  console.error('Error writing file:', error);
}

When to Use Synchronous Writing

Use synchronous writing when:

  • Building CLI tools
  • Writing configuration files during startup
  • Simple scripts where blocking is acceptable
  • Testing and debugging

Avoid synchronous writing when:

  • Building web servers - blocks the event loop, preventing other requests from being processed, making the server unresponsive
  • Handling user requests - causes request blocking where one user’s file operation delays all other users’ requests
  • Any scenario where performance matters - eliminates concurrency, wastes CPU cycles, and creates unnecessary bottlenecks
  • Working with large files - can block for seconds or minutes, risking timeouts and exhausting system resources

File Writing Options and Flags

Understanding File Flags

File flags control how the file is opened and written:

import { writeFile } from 'fs/promises';

// Default behavior - overwrites existing file
await writeFile('file.txt', 'New content');

// Append to existing file
await writeFile('file.txt', 'Additional content', { flag: 'a' });

// Create file only if it doesn't exist
await writeFile('file.txt', 'Content', { flag: 'wx' });

// Read and write (r+)
await writeFile('file.txt', 'Content', { flag: 'r+' });

Common File Flags

| Flag | Description | |——|————-| | 'w' | Write (default) - overwrites existing file | | 'a' | Append - adds to existing file | | 'wx' | Write exclusive - fails if file exists | | 'r+' | Read and write - fails if file doesn’t exist | | 'w+' | Write and read - creates file if it doesn’t exist |

Encoding Options

import { writeFile } from 'fs/promises';

// UTF-8 (default)
await writeFile('utf8.txt', 'Hello World', { encoding: 'utf8' });

// ASCII
await writeFile('ascii.txt', 'Hello World', { encoding: 'ascii' });

// Binary
await writeFile('binary.txt', Buffer.from('Hello World'), { encoding: 'binary' });

File Permissions

import { writeFile } from 'fs/promises';

// Set specific permissions (Unix-like systems)
await writeFile('secure.txt', 'Secret data', { 
  mode: 0o600  // Owner read/write only
});

// Common permission modes:
// 0o644 - Owner read/write, others read
// 0o600 - Owner read/write only
// 0o755 - Owner read/write/execute, others read/execute

Appending Content to Files

Using fs.appendFile()

import { appendFile } from 'fs/promises';

// Append to existing file
await appendFile('log.txt', `\n${new Date().toISOString()}: New log entry`);

// Append with options
await appendFile('log.txt', 'Another entry', { encoding: 'utf8' });

Appending vs Writing

import { writeFile, appendFile } from 'fs/promises';

// Write (overwrites entire file)
await writeFile('data.txt', 'Line 1\nLine 2\nLine 3');

// Append (adds to existing content)
await appendFile('data.txt', '\nLine 4\nLine 5');

// Result: data.txt contains all 5 lines

Advanced File Writing Techniques

Writing Large Files with Streams

For large files, streams are more memory-efficient than loading everything into memory:

import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

async function writeLargeFile(): Promise<void> {
  const writeStream = createWriteStream('large-file.txt');
  
  // Write data in chunks
  for (let i = 0; i < 1_000_000; i++) {
    const chunk = `Line ${i}: Some data here\n`;
    if (!writeStream.write(chunk)) {
      // Wait for drain event if buffer is full
      await new Promise<void>(resolve => writeStream.once('drain', resolve));
    }
  }
  
  writeStream.end();
}

writeLargeFile();

Using Pipeline for Streams

import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

async function copyFile(source: string, destination: string): Promise<void> {
  try {
    await pipeline(
      createReadStream(source),
      createWriteStream(destination)
    );
    console.log('File copied successfully');
  } catch (error) {
    console.error('Error copying file:', error);
  }
}

copyFile('source.txt', 'destination.txt');

Writing Multiple Files Concurrently

import { writeFile } from 'fs/promises';

interface FileData {
  name: string;
  content: string;
}

async function writeMultipleFiles(): Promise<void> {
  const files: FileData[] = [
    { name: 'file1.txt', content: 'Content 1' },
    { name: 'file2.txt', content: 'Content 2' },
    { name: 'file3.txt', content: 'Content 3' }
  ];
  
  try {
    await Promise.all(
      files.map(file => 
        writeFile(file.name, file.content)
      )
    );
    console.log('All files written successfully');
  } catch (error) {
    console.error('Error writing files:', error);
  }
}

Error Handling Strategies

Comprehensive Error Handling

import { writeFile } from 'fs/promises';

async function writeFileWithErrorHandling(filename: string, content: string | Buffer): Promise<void> {
  try {
    await writeFile(filename, content);
    console.log(`File ${filename} written successfully`);
  } catch (error) {
    if (error instanceof Error && 'code' in error) {
      switch (error.code) {
        case 'ENOSPC':
          console.error('No space left on device');
          break;
        case 'EACCES':
          console.error('Permission denied - check file permissions');
          break;
        case 'ENOENT':
          console.error('Directory does not exist');
          break;
        case 'EISDIR':
          console.error('Path is a directory, not a file');
          break;
        default:
          console.error('Unexpected error:', error.message);
      }
    } else {
      console.error('Unexpected error:', error);
    }
    throw error; // Re-throw for caller to handle
  }
}

Retry Logic for File Operations

import { writeFile } from 'fs/promises';

async function writeFileWithRetry(
  filename: string, 
  content: string | Buffer, 
  maxRetries: number = 3
): Promise<void> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await writeFile(filename, content);
      console.log(`File written successfully on attempt ${attempt}`);
      return;
    } catch (error) {
      if (attempt === maxRetries) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        throw new Error(`Failed to write file after ${maxRetries} attempts: ${errorMessage}`);
      }
      
      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise<void>(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
    }
  }
}

Working with File Paths

Using the Path Module

import { writeFile, mkdir } from 'fs/promises';
import path from 'path';

// Create absolute paths
const filePath = path.join(__dirname, 'data', 'output.txt');

// Ensure directory exists before writing
await mkdir(path.dirname(filePath), { recursive: true });

// Write to the file
await writeFile(filePath, 'File content');

Performance Optimization

Buffering Strategies

import { writeFile } from 'fs/promises';

// Write multiple small writes as one operation
async function writeEfficiently(filename: string, lines: string[]): Promise<void> {
  const content = lines.join('\n');
  await writeFile(filename, content);
}

// Instead of:
// for (const line of lines) {
//   await appendFile(filename, line + '\n');
// }

Batch Processing

import { writeFile } from 'fs/promises';

interface FileData {
  name: string;
  content: string;
}

async function batchWriteFiles(files: FileData[], batchSize: number = 10): Promise<void> {
  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize);
    
    await Promise.all(
      batch.map(file => 
        writeFile(file.name, file.content)
      )
    );
    
    console.log(`Processed batch ${Math.floor(i / batchSize) + 1}`);
  }
}

Best Practices Summary

Do’s

  • ✅ Use promise-based APIs for modern applications
  • ✅ Handle errors comprehensively
  • ✅ Use appropriate file flags for your use case
  • ✅ Use streams for large files
  • ✅ Set appropriate file permissions
  • ✅ Use absolute paths when possible

Don’ts

  • ❌ Don’t use synchronous methods in production servers
  • ❌ Don’t ignore file writing errors
  • ❌ Don’t write files without checking permissions
  • ❌ Don’t load large files entirely into memory
  • ❌ Don’t forget to close file streams

Common Use Cases

1. Logging

import { appendFile } from 'fs/promises';

async function logMessage(message: string, level: string = 'INFO') {
  const timestamp = new Date().toISOString();
  const logEntry = `[${timestamp}] ${level}: ${message}\n`;
  
  await appendFile('app.log', logEntry);
}

2. Configuration Files

import { writeFile } from 'fs/promises';

async function saveConfig(config: Record<string, any>){
  const configContent = JSON.stringify(config, null, 2);
  await writeFile('config.json', configContent);
}

3. Data Export

import { writeFile } from 'fs/promises';

async function exportToCSV(data: Record<string, any>[], filename: string) {
  const headers = Object.keys(data[0]).join(',');
  const rows = data.map(row => Object.values(row).join(','));
  const csvContent = [headers, ...rows].join('\n');
  
  await writeFile(filename, csvContent);
}

Conclusion

File writing in Node.js is straightforward once you understand the different APIs and their trade-offs. The promise-based fs.promises API provides the best balance of performance and readability for most applications.

Key takeaways:

  • Use promises for modern, clean code
  • Handle errors comprehensively
  • Choose the right method for your use case
  • Consider performance for large files

If this article was helpful, tweet it!