Skip to main content

File System (fs)

The node:fs module enables interacting with the file system in a way modeled on standard POSIX functions.

Import

// Promise-based API (recommended)
import * as fs from 'node:fs/promises';

// Callback and sync API
import * as fs from 'node:fs';

API Forms

All file system operations have three forms:
  1. Promises API - Returns promises (recommended for async operations)
  2. Callback API - Takes completion callbacks as last argument
  3. Synchronous API - Blocks the event loop until operation completes

Promise Example

import { unlink } from 'node:fs/promises';

try {
  await unlink('/tmp/hello');
  console.log('successfully deleted /tmp/hello');
} catch (error) {
  console.error('there was an error:', error.message);
}

Callback Example

import { unlink } from 'node:fs';

unlink('/tmp/hello', (err) => {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});

Synchronous Example

import { unlinkSync } from 'node:fs';

try {
  unlinkSync('/tmp/hello');
  console.log('successfully deleted /tmp/hello');
} catch (err) {
  // handle the error
}

File Descriptors

On POSIX systems, for every process, the kernel maintains a table of currently open files and resources. Each open file is assigned a simple numeric identifier called a file descriptor.

Working with File Descriptors

import { open, close, read } from 'node:fs/promises';

let filehandle;
try {
  filehandle = await open('thefile.txt', 'r');
  const buffer = Buffer.alloc(16384);
  const { bytesRead } = await filehandle.read(buffer, 0, 16384);
  console.log(`Read ${bytesRead} bytes`);
} finally {
  await filehandle?.close();
}

Core Methods

Reading Files

fs.readFile(path[, options])

Asynchronously reads the entire contents of a file. Parameters:
  • path - Filename or file descriptor
  • options - Encoding, flag, or signal options
Returns: Promise with file contents
import { readFile } from 'node:fs/promises';

const data = await readFile('package.json', { encoding: 'utf8' });
console.log(data);

fs.readFileSync(path[, options])

Synchronous version of readFile().
import { readFileSync } from 'node:fs';

const data = readFileSync('package.json', 'utf8');

fs.read(fd, buffer, offset, length, position)

Reads data from a file descriptor into a buffer.
import { open } from 'node:fs/promises';

const fd = await open('file.txt', 'r');
const buffer = Buffer.alloc(1024);
const { bytesRead } = await fd.read(buffer, 0, 1024, 0);
await fd.close();

Writing Files

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

Asynchronously writes data to a file, replacing it if it already exists.
import { writeFile } from 'node:fs/promises';

await writeFile('message.txt', 'Hello Node.js');

fs.appendFile(path, data[, options])

Appends data to a file, creating the file if it doesn’t exist.
import { appendFile } from 'node:fs/promises';

await appendFile('log.txt', 'new log entry\n');

fs.write(fd, buffer, offset, length, position)

Writes buffer to the file descriptor.
import { open } from 'node:fs/promises';

const fd = await open('file.txt', 'w');
const buffer = Buffer.from('Hello World');
await fd.write(buffer, 0, buffer.length);
await fd.close();

File Operations

fs.open(path, flags[, mode])

Opens a file and returns a file handle. Flags:
  • 'r' - Open for reading (default)
  • 'w' - Open for writing (truncates file)
  • 'a' - Open for appending
  • 'r+' - Open for reading and writing
  • 'w+' - Open for reading and writing (truncates)
  • 'a+' - Open for reading and appending
import { open } from 'node:fs/promises';

const filehandle = await open('file.txt', 'r');
// Use filehandle
await filehandle.close();

fs.close(fd)

Closes a file descriptor.

fs.rename(oldPath, newPath)

Renames a file or directory.
import { rename } from 'node:fs/promises';

await rename('old.txt', 'new.txt');
Removes a file or symbolic link.
import { unlink } from 'node:fs/promises';

await unlink('file.txt');

fs.copyFile(src, dest[, mode])

Copies a file from source to destination.
import { copyFile } from 'node:fs/promises';

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

Directory Operations

fs.mkdir(path[, options])

Creates a directory.
import { mkdir } from 'node:fs/promises';

// Create single directory
await mkdir('newdir');

// Create nested directories
await mkdir('parent/child/grandchild', { recursive: true });

fs.readdir(path[, options])

Reads the contents of a directory.
import { readdir } from 'node:fs/promises';

// Get file names
const files = await readdir('.');

// Get Dirent objects with file type info
const entries = await readdir('.', { withFileTypes: true });
for (const entry of entries) {
  console.log(`${entry.name}: ${entry.isDirectory() ? 'dir' : 'file'}`);
}

// Recursive directory reading
const allFiles = await readdir('.', { recursive: true });

fs.rmdir(path[, options])

Removes a directory.
import { rmdir } from 'node:fs/promises';

await rmdir('emptydir');

fs.rm(path[, options])

Removes files and directories (similar to POSIX rm).
import { rm } from 'node:fs/promises';

// Remove file
await rm('file.txt');

// Remove directory recursively
await rm('directory', { recursive: true, force: true });

File Stats

fs.stat(path[, options])

Retrieves file system statistics.
import { stat } from 'node:fs/promises';

const stats = await stat('file.txt');

console.log(stats.isFile());       // true
console.log(stats.isDirectory());  // false
console.log(stats.size);           // File size in bytes
console.log(stats.mtime);          // Modified time

fs.lstat(path[, options])

Like stat() but returns info about symbolic link itself, not the target.

fs.fstat(fd[, options])

Returns stats for an open file descriptor.

Permissions and Ownership

fs.chmod(path, mode)

Changes file permissions.
import { chmod } from 'node:fs/promises';

await chmod('file.txt', 0o755);

fs.chown(path, uid, gid)

Changes file ownership.
import { chown } from 'node:fs/promises';

await chown('file.txt', 1000, 1000);
Creates a symbolic link.
import { symlink } from 'node:fs/promises';

await symlink('target.txt', 'link.txt');
Reads the contents of a symbolic link.
import { readlink } from 'node:fs/promises';

const target = await readlink('link.txt');
Creates a hard link.
import { link } from 'node:fs/promises';

await link('file.txt', 'hardlink.txt');

File Streams

fs.createReadStream(path[, options])

Creates a readable stream.
import { createReadStream } from 'node:fs';

const stream = createReadStream('file.txt', { encoding: 'utf8' });
stream.on('data', (chunk) => console.log(chunk));
stream.on('end', () => console.log('Done reading'));

fs.createWriteStream(path[, options])

Creates a writable stream.
import { createWriteStream } from 'node:fs';

const stream = createWriteStream('output.txt');
stream.write('Hello ');
stream.write('World');
stream.end();

FileHandle Class

The FileHandle object is a wrapper for a numeric file descriptor.

Methods

  • filehandle.appendFile(data[, options]) - Appends to file
  • filehandle.chmod(mode) - Changes permissions
  • filehandle.chown(uid, gid) - Changes ownership
  • filehandle.close() - Closes file handle
  • filehandle.read(buffer, offset, length, position) - Reads data
  • filehandle.readFile([options]) - Reads entire file
  • filehandle.stat([options]) - Gets file stats
  • filehandle.sync() - Flushes data to storage
  • filehandle.truncate(len) - Truncates file
  • filehandle.write(buffer, offset, length, position) - Writes data
  • filehandle.writeFile(data[, options]) - Writes entire file

Properties

  • filehandle.fd - The numeric file descriptor

Performance Considerations

Sync vs Async vs Promises

  • Synchronous: Blocks event loop - use only during app startup or in worker threads
  • Callbacks: Best performance for high-throughput scenarios
  • Promises: Modern, easier to use, slight overhead compared to callbacks

Streams for Large Files

Use streams instead of readFile()/writeFile() for large files to avoid memory issues:
import { createReadStream, createWriteStream } from 'node:fs';

const reader = createReadStream('large-file.txt');
const writer = createWriteStream('copy.txt');
reader.pipe(writer);

Error Handling

Promise-based

import { readFile } from 'node:fs/promises';

try {
  const data = await readFile('file.txt', 'utf8');
} catch (err) {
  if (err.code === 'ENOENT') {
    console.error('File not found');
  } else {
    throw err;
  }
}

Callback-based

import { readFile } from 'node:fs';

readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.error('File not found');
      return;
    }
    throw err;
  }
  console.log(data);
});

Common Error Codes

  • EACCES - Permission denied
  • EEXIST - File already exists
  • EISDIR - Is a directory
  • ENOENT - No such file or directory
  • ENOTDIR - Not a directory
  • ENOTEMPTY - Directory not empty
  • EPERM - Operation not permitted

Best Practices

  1. Always close file handles when done
  2. Use promises API for modern async code
  3. Use streams for large files
  4. Avoid sync methods in production servers
  5. Handle errors appropriately
  6. Use path.join() for cross-platform path handling
  7. Set proper file permissions
  8. Use AbortSignal for cancellable operations

See Also

  • Path API - Path manipulation utilities
  • OS API - Operating system utilities
  • Streams - Stream interface documentation