04 - File System

📋 Jump to Takeaways

This lesson uses ES Modules. Make sure your package.json has "type": "module". See Modules if you need a refresher.

Reading Files

The fs module handles all file operations. Two flavors: callback-based and promise-based.

// Callback style (legacy)
import fs from 'node:fs';

fs.readFile('data.txt', 'utf-8', (err, data) => {
  if (err) throw err;
  console.log(data);
});
// Promise style (recommended)
import fs from 'node:fs/promises';

const data = await fs.readFile('data.txt', 'utf-8');
console.log(data);

Always use fs/promises in new code. Callbacks are legacy.

In CommonJS projects, you'll need an async wrapper since top-level await isn't available:

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

async function main() {
  const data = await fs.readFile('data.txt', 'utf-8');
  console.log(data);
}
main();

Writing Files

import fs from 'node:fs/promises';

// Write (creates or overwrites)
await fs.writeFile('output.txt', 'Hello, Node!');

// Append (adds to end)
await fs.appendFile('log.txt', 'New entry\n');

writeFile replaces the entire file. Use appendFile to add to it.

Checking if Files Exist

The simplest way is fs.existsSync(). It returns a boolean.

import fs from 'node:fs';

if (fs.existsSync('config.json')) {
  console.log('File exists');
}

For async code, use fs.access() with try-catch:

import fs from 'node:fs/promises';

try {
  await fs.access('config.json');
  console.log('File exists');
} catch {
  console.log('File does not exist');
}
  • Don't use the deprecated fs.exists().
  • Use existsSync for quick checks.
  • Use fs.access() when you're inside async code like a server.

Working with Directories

import fs from 'node:fs/promises';

// Create a directory
await fs.mkdir('logs');

// Create nested directories
await fs.mkdir('data/backups/2026', { recursive: true });

// List directory contents
const files = await fs.readdir('.');
console.log(files); // ['index.js', 'package.json', 'node_modules']

// Remove a directory
await fs.rm('logs', { recursive: true });

// Remove directory with contents
await fs.rm('data', { recursive: true });

File Info

import fs from 'node:fs/promises';

const stats = await fs.stat('index.js');
console.log(stats.isFile());      // true
console.log(stats.isDirectory()); // false
console.log(stats.size);          // 1234 (bytes)

The path Module

Never build file paths with string concatenation. Use path.

import path from 'node:path';

path.join('users', 'data', 'file.txt');
// "users/data/file.txt" (macOS/Linux)
// "users\\data\\file.txt" (Windows)

path.resolve('src', 'index.js');
// "/Users/you/project/src/index.js" (absolute path)

path.basename('/Users/you/project/app.js');
// "app.js"

path.extname('photo.png');
// ".png"

path.dirname('/Users/you/project/app.js');
// "/Users/you/project"

path.join handles slashes across operating systems. path.resolve gives you an absolute path.

Wrap each statement in console.log to check the output.

Sync vs Async

Every fs method has a sync version. It blocks the entire process.

import fs from 'node:fs';

// Sync (blocking) - only for startup scripts
const data = fs.readFileSync('data.txt', 'utf-8');

Sync methods are fine for CLI tools or one-time setup. Never use them in a server handling requests.

Key Takeaways

  • Use fs/promises for async file operations, not callbacks
  • High-level methods like readFile and writeFile handle opening and closing files for you
  • writeFile overwrites, appendFile adds to the end
  • Use existsSync or fs.access() to check if a file exists, not the deprecated fs.exists()
  • mkdir with { recursive: true } creates nested directories
  • Always use path.join() or path.resolve() for file paths, never string concatenation
  • Sync methods block the process, only use them in scripts, never in servers

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy