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.
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.
Let Node.js handle file descriptor lifecycle automatically by using the default autoClose behavior:
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.
If creating a stream from a FileHandle, let the stream manage the lifecycle:
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.
If you need manual control, use autoClose: false and ensure proper sequencing:
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.
Guard against double-close by checking the descriptor state:
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.
Prevent race conditions by ensuring operations complete before closing:
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.
Check and increase system limits if running out of file descriptors:
# 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 8192For Node.js applications in production:
// 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.
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:
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:
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.
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: EACCES: permission denied, open '/root/file.txt'
EACCES: permission denied
Error: Invalid encoding specified (stream encoding not supported)
How to fix Invalid encoding error in Node.js readable streams
Error: EINVAL: invalid argument, open
EINVAL: invalid argument, open
TypeError: readableLength must be a positive integer (stream config)
TypeError: readableLength must be a positive integer in Node.js streams