How to fix Error: EBADF: bad file descriptor, read (file descriptor closed) in Node.js

Node.jsINTERMEDIATEMEDIUMUpdated Dec 9, 2025· 239 views

This error occurs when Node.js attempts to perform I/O operations on a file descriptor that has already been closed or is invalid. It commonly happens with double-close scenarios, premature stream closures, or attempting to read/write after a file handle has been released.

Quick fix

Fixes EBADF

const fs = require('fs');

// autoClose defaults to true - stream handles closing
const readStream = fs.createReadStream('/path/to/file.txt');

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

readStream.on('end', () => {
  console.log('Stream finished, descriptor auto-closed');
});

readStream.on('error', (err) => {
  console.error('Stream error:', err);
  // Stream will still auto-close on error
});
  1. Use autoClose: true for streams (default behavior)
  2. Avoid explicit close when using streams from file handles
  3. Set autoClose: false only when managing descriptor manually
  4. Check if descriptor is already closed before closing
node.js — error
const fs = require('fs');
// autoClose defaults to true - stream handles closing
const readStream = fs.createReadStream('/path/to/file.txt');
readStream.on('data', (chunk) => {
console.log('Read:', chunk.length, 'bytes');
});
readStream.on('end', () => {
console.log('Stream finished, descriptor auto-closed');
});
readStream.on('error', (err) => {
console.error('Stream error:', err);
// Stream will still auto-close on error
});
Error: EBADF: bad file descriptor, read (file descriptor closed)

What this error means

The EBADF (Bad File Descriptor) error is a system-level error indicating that Node.js attempted to perform an operation (read, write, or close) on a file descriptor that is no longer valid. A file descriptor is a low-level reference to an open file or stream, and once it's closed, any subsequent operations on it will fail with this error. This error most frequently occurs in scenarios where file handles are closed prematurely while streams are still active, when close() is called multiple times on the same descriptor, or when there's a race condition between closing and ongoing I/O operations. It's particularly common when working with the fs/promises API or when manually managing file descriptors. The error represents a programming logic issue rather than a file system problem. It indicates that the application's resource management needs adjustment—either by ensuring descriptors aren't closed too early, by properly coordinating stream lifecycle with file handle lifecycle, or by preventing duplicate close operations.

Typical symptoms

  • Error message "EBADF: bad file descriptor, read" during file operations
  • Stream operations fail with EBADF after explicitly closing file handle
  • Error occurs intermittently under high load or concurrent file access
  • Application crashes with EBADF when using fs.createReadStream or fs.createWriteStream
  • Error thrown when calling close() multiple times on the same file descriptor
  • EBADF error appears after "close" event in stream lifecycle

Common causes

  1. Closing a file handle while a stream created from it is still reading or writing
  2. Calling fs.close() or handle.close() multiple times on the same descriptor
  3. Stream with autoClose: false leaves descriptor open, then explicit close happens while I/O continues
  4. Race condition where close operation executes before pending read/write completes
  5. Using a file descriptor after the FileHandle has been closed in fs/promises
  6. Hitting system file descriptor limits causing operations to fail on invalid descriptors
  7. Destroying a stream that internally tries to close an already-closed descriptor

How to fix "Error: EBADF: bad file descriptor, read (file descriptor closed)"

1Use autoClose: true for streams (default behavior)

Let Node.js handle file descriptor lifecycle automatically by using the default autoClose behavior:

javascript
const fs = require('fs');

// autoClose defaults to true - stream handles closing
const readStream = fs.createReadStream('/path/to/file.txt');

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

readStream.on('end', () => {
  console.log('Stream finished, descriptor auto-closed');
});

readStream.on('error', (err) => {
  console.error('Stream error:', err);
  // Stream will still auto-close on error
});

With autoClose: true, the file descriptor is automatically closed when the stream ends or errors, preventing manual close conflicts.

2Avoid explicit close when using streams from file handles

If creating a stream from a FileHandle, let the stream manage the lifecycle:

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

async function readFile() {
  const fileHandle = await fs.open('/path/to/file.txt', 'r');

  // Create stream from file handle
  const stream = fileHandle.createReadStream();

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

  // Wait for stream to finish - DON'T close fileHandle here
  await new Promise((resolve, reject) => {
    stream.on('end', resolve);
    stream.on('error', reject);
  });

  // Now safe to close
  await fileHandle.close();
}

The stream must complete before closing the underlying file handle to avoid EBADF errors.

3Set autoClose: false only when managing descriptor manually

If you need manual control, use autoClose: false and ensure proper sequencing:

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

async function manualControl() {
  const fileHandle = await fs.open('/path/to/file.txt', 'r');

  try {
    const stream = fileHandle.createReadStream({
      autoClose: false // We'll close manually
    });

    // Process stream
    for await (const chunk of stream) {
      console.log('Processing:', chunk.length, 'bytes');
    }

    // Stream finished, now safe to close
    await fileHandle.close();
    console.log('File handle closed successfully');

  } catch (err) {
    console.error('Error:', err);
    await fileHandle.close(); // Close even on error
  }
}

Only close the file handle after confirming the stream has finished processing.

4Check if descriptor is already closed before closing

Guard against double-close by checking the descriptor state:

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

let fileHandle = null;

async function safeClose() {
  if (fileHandle && !fileHandle.closed) {
    try {
      await fileHandle.close();
      console.log('File handle closed');
    } catch (err) {
      if (err.code !== 'EBADF') {
        throw err; // Re-throw non-EBADF errors
      }
      // EBADF means already closed, ignore
      console.log('Descriptor already closed');
    }
    fileHandle = null;
  }
}

async function openAndRead() {
  try {
    fileHandle = await fs.open('/path/to/file.txt', 'r');
    const content = await fileHandle.readFile('utf8');
    console.log(content);
  } finally {
    await safeClose();
  }
}

The closed property indicates whether the FileHandle has been closed.

5Handle concurrent operations with proper synchronization

Prevent race conditions by ensuring operations complete before closing:

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

async function concurrentSafeRead() {
  const fileHandle = await fs.open('/path/to/file.txt', 'r');

  try {
    // Multiple reads - wait for all to complete
    const results = await Promise.all([
      fileHandle.read(Buffer.alloc(100), 0, 100, 0),
      fileHandle.read(Buffer.alloc(100), 0, 100, 100),
      fileHandle.read(Buffer.alloc(100), 0, 100, 200),
    ]);

    console.log('All reads completed:', results);

  } finally {
    // Only close after all operations finish
    await fileHandle.close();
  }
}

Use Promise.all() to ensure all I/O operations complete before closing the descriptor.

6Increase system file descriptor limits if hitting limits

Check and increase system limits if running out of file descriptors:

bash
# Check current limit
ulimit -n

# Increase soft limit temporarily (until reboot)
ulimit -n 4096

# For permanent change, edit /etc/security/limits.conf
# Add these lines:
* soft nofile 4096
* hard nofile 8192

For Node.js applications in production:

javascript
// Check current limits
const { exec } = require('child_process');

exec('ulimit -n', (err, stdout) => {
  console.log('File descriptor limit:', stdout.trim());
});

High-concurrency applications may need higher limits to prevent descriptor exhaustion.

Advanced notes

Understanding File Descriptor Lifecycle with Streams

When using fs/promises with streams, there's a critical timing consideration. The fs.open() returns a FileHandle, and creating a stream from it doesn't transfer ownership. Both the stream and the FileHandle reference the same underlying descriptor:

javascript
const fileHandle = await fs.open('file.txt', 'r');
const stream = fileHandle.createReadStream();
// Two references to the same descriptor!

If you call fileHandle.close() while the stream is reading, EBADF occurs because the stream tries to use a closed descriptor.

Node.js Version Differences

Node.js v15+ changed behavior around EBADF errors in streams. Earlier versions would sometimes silently ignore double-close attempts, while newer versions throw EBADF more consistently. A PR (nodejs/node#11225) specifically addressed "avoid emitting error EBADF for double close" but the fundamental requirement remains: don't operate on closed descriptors.

Debugging EBADF in Production

To identify where descriptors are being mismanaged:

javascript
const fs = require('fs/promises');
const originalOpen = fs.open;

fs.open = async function(...args) {
  const handle = await originalOpen.apply(this, args);
  const stack = new Error().stack;

  console.log('FileHandle opened:', {
    path: args[0],
    fd: handle.fd,
    stack
  });

  const originalClose = handle.close.bind(handle);
  handle.close = async function() {
    console.log('Closing fd:', handle.fd);
    console.trace('Close stack trace');
    return originalClose();
  };

  return handle;
};

This wrapper logs every open/close with stack traces to identify double-close patterns.

Stream Destroy vs End

Understanding the difference prevents EBADF errors:

- stream.end() - Signals no more data will be written, allows pending I/O to complete
- stream.destroy() - Immediately terminates, may trigger EBADF if descriptor closed prematurely

Always prefer end() for graceful shutdown unless immediate termination is required.

Platform-Specific Behavior

EBADF behavior can vary slightly between operating systems:
- Linux: More strict, throws EBADF consistently on invalid descriptor operations
- macOS: Similar to Linux, but some race conditions harder to reproduce
- Windows: Uses handles instead of file descriptors, but similar "bad handle" errors

Always test file handling code across target platforms, especially when deploying containerized applications.

Frequently asked questions

What does "EBADF" mean?
js attempted to perform an operation (read, write, or close) on a file descriptor that is no longer valid. A file descriptor is a low-level reference to an open file or stream, and once it's closed, any subsequent operations on it will fail with this error. This error most frequently occurs in scenarios where file handles are closed prematurely while streams are still active, when close() is called multiple times on the same descriptor, or when there's a race condition between closing and ongoing I/O operations.
How do I fix "EBADF"?
This error occurs when Node.js attempts to perform I/O operations on a file descriptor that has already been closed or is invalid. It commonly happens with double-close scenarios, premature stream closures, or attempting to read/write after a file handle has been released.
What causes "EBADF"?
Closing a file handle while a stream created from it is still reading or writing Calling fs.close() or handle.close() multiple times on the same descriptor Stream with autoClose: false leaves descriptor open, then explicit close happens while I/O continues
Is the "EBADF" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading