How to fix Error: Duplex stream ended unexpectedly (both readable and writable failed) in Node.js

Node.jsADVANCEDHIGHUpdated Dec 9, 2025ยท 143 views

This error occurs when both sides of a Node.js duplex stream (readable and writable) fail or close prematurely before completing data transfer. It commonly happens due to improper error handling, missing event listeners, or abrupt connection terminations in network streams, transform streams, or bidirectional pipes.

Quick fix

Fixes Error: Duplex stream ended unexpectedly (both readable and writable failed)

const { Transform } = require('stream');

const transformStream = new Transform({
  transform(chunk, encoding, callback) {
    try {
      // Transform logic here
      const result = processData(chunk);
      callback(null, result);
    } catch (err) {
      callback(err);
    }
  }
});

// Critical: listen for errors
transformStream.on('error', (err) => {
  console.error('Transform stream error:', err);
  // Handle error gracefully
});

transformStream.on('end', () => {
  console.log('Stream ended successfully');
});

transformStream.on('close', () => {
  console.log('Stream closed');
});
  1. Add error listeners to all stream instances
  2. Use pipeline() instead of pipe() for error handling
  3. Implement proper backpressure handling
  4. Check stream state before operations
node.js โ€” error
const { Transform } = require('stream');
const transformStream = new Transform({
transform(chunk, encoding, callback) {
try {
// Transform logic here
const result = processData(chunk);
callback(null, result);
} catch (err) {
callback(err);
}
}
});
// Critical: listen for errors
transformStream.on('error', (err) => {
console.error('Transform stream error:', err);
// Handle error gracefully
});
transformStream.on('end', () => {
console.log('Stream ended successfully');
});
transformStream.on('close', () => {
console.log('Stream closed');
});
Error: Duplex stream ended unexpectedly (both readable and writable failed)

What this error means

A duplex stream error indicates that a bidirectional stream closed unexpectedly on both the readable and writable sides before data transfer completed. Duplex streams maintain two independent internal buffers for reading and writing, allowing simultaneous bidirectional communication like TCP sockets or HTTP/2 connections. When both sides of a duplex stream fail, it typically means an unhandled error propagated through the stream chain, a connection was abruptly terminated, or proper cleanup wasn't performed. Unlike readable-only or writable-only streams, duplex streams require careful coordination between both directions to prevent premature termination. This error is particularly common when piping streams together without proper error handling, when network connections drop unexpectedly, or when transform streams encounter errors during data processing. Understanding duplex stream lifecycle management and event-driven error handling is essential for building robust streaming applications.

Typical symptoms

  • Error message includes "Duplex stream ended unexpectedly"
  • Both readable and writable stream operations fail simultaneously
  • Data transfer stops abruptly before completion
  • Network socket connections drop with stream errors
  • Transform streams stop processing mid-operation
  • Piped stream chains break unexpectedly
  • Unhandled error events from duplex streams
  • Application crashes when stream errors are not caught

Common causes

  1. Missing error event listeners on duplex or transform streams
  2. Network connection closed abruptly by client or server
  3. Unhandled exceptions in transform stream _transform() method
  4. Improper use of stream.destroy() without error parameter
  5. Backpressure not handled correctly causing buffer overflow
  6. Piping streams without pipeline() or proper error propagation
  7. Attempting to write to a stream after it has ended
  8. Reading from a stream that has been destroyed or closed
  9. Memory pressure causing stream to abort prematurely

How to fix "Error: Duplex stream ended unexpectedly (both readable and writable failed)"

1Add error listeners to all stream instances

Always attach error event listeners to duplex and transform streams:

javascript
const { Transform } = require('stream');

const transformStream = new Transform({
  transform(chunk, encoding, callback) {
    try {
      // Transform logic here
      const result = processData(chunk);
      callback(null, result);
    } catch (err) {
      callback(err);
    }
  }
});

// Critical: listen for errors
transformStream.on('error', (err) => {
  console.error('Transform stream error:', err);
  // Handle error gracefully
});

transformStream.on('end', () => {
  console.log('Stream ended successfully');
});

transformStream.on('close', () => {
  console.log('Stream closed');
});

Without error listeners, unhandled errors will cause both sides of the duplex stream to fail.

2Use pipeline() instead of pipe() for error handling

Replace manual pipe() calls with pipeline() for automatic error propagation:

javascript
const { pipeline } = require('stream/promises');
const fs = require('fs');
const zlib = require('zlib');

// Bad: No error handling
// readStream.pipe(gzip).pipe(writeStream);

// Good: Automatic error handling
async function processFiles() {
  try {
    await pipeline(
      fs.createReadStream('input.txt'),
      zlib.createGzip(),
      fs.createWriteStream('output.txt.gz')
    );
    console.log('Pipeline completed successfully');
  } catch (err) {
    console.error('Pipeline failed:', err);
    // All streams are properly cleaned up
  }
}

processFiles();

pipeline() automatically destroys all streams when any stream fails, preventing resource leaks.

3Implement proper backpressure handling

Respect the return value of write() to handle backpressure correctly:

javascript
const { Duplex } = require('stream');

const duplexStream = new Duplex({
  write(chunk, encoding, callback) {
    // Process write
    this.processWrite(chunk);
    callback();
  },
  read(size) {
    // Provide data for reading
    this.push(this.getData());
  }
});

// Handle backpressure when writing
function writeData(stream, data) {
  const canContinue = stream.write(data);

  if (!canContinue) {
    console.log('Backpressure detected, waiting for drain...');
    stream.once('drain', () => {
      console.log('Stream drained, can continue writing');
    });
  }
}

// Check if stream is writable before writing
if (duplexStream.writable) {
  writeData(duplexStream, 'some data');
}

Ignoring backpressure can cause memory buildup and stream failures.

4Check stream state before operations

Verify stream readiness before reading or writing:

javascript
function safeWrite(stream, data) {
  // Check if stream is still writable
  if (!stream.writable || stream.destroyed) {
    console.error('Stream is not writable');
    return false;
  }

  // Check if stream has ended
  if (stream.writableEnded) {
    console.error('Stream has already ended');
    return false;
  }

  return stream.write(data);
}

function safeRead(stream) {
  // Check if stream is still readable
  if (!stream.readable || stream.destroyed) {
    console.error('Stream is not readable');
    return null;
  }

  // Check if stream has ended
  if (stream.readableEnded) {
    console.error('Stream has already ended');
    return null;
  }

  return stream.read();
}

This prevents operations on closed or destroyed streams.

5Implement graceful stream cleanup and destruction

Properly clean up streams when errors occur or operations complete:

javascript
const { Duplex } = require('stream');

class CustomDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.resources = [];
  }

  _construct(callback) {
    // Initialize resources
    this.setupResources();
    callback();
  }

  _destroy(error, callback) {
    // Clean up resources
    console.log('Cleaning up duplex stream resources');
    this.resources.forEach(resource => resource.close());
    this.resources = [];

    // Call callback with error if present
    callback(error);
  }

  write(chunk, encoding, callback) {
    // Check state before writing
    if (this.destroyed) {
      const err = new Error('Cannot write to destroyed stream');
      if (callback) callback(err);
      return false;
    }
    return super.write(chunk, encoding, callback);
  }
}

// Usage with proper error handling
const stream = new CustomDuplex();

stream.on('error', (err) => {
  console.error('Stream error:', err);
  stream.destroy(err); // Ensure cleanup happens
});

// Graceful shutdown
process.on('SIGTERM', () => {
  stream.destroy(); // Clean shutdown
});

Proper cleanup prevents resource leaks and cascading errors.

Advanced notes

Creating Duplex Streams from Separate Readable and Writable

You can create a duplex stream from independent readable and writable streams:

javascript
const { Duplex } = require('stream');
const { Readable, Writable } = require('stream');

const readable = Readable.from(['chunk1', 'chunk2', 'chunk3']);
const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log('Received:', chunk.toString());
    callback();
  }
});

// Combine into duplex stream
const duplex = Duplex.from({
  readable,
  writable
});

// Handle errors from both sides
duplex.on('error', (err) => {
  console.error('Duplex error:', err);
});

Duplex Stream Backpressure Management

When implementing custom duplex streams, handle backpressure on both sides:

javascript
const { Duplex } = require('stream');

const duplexStream = new Duplex({
  highWaterMark: 16384, // Buffer size for backpressure

  read(size) {
    // Called when consumer is ready for more data
    if (this.hasMoreData()) {
      this.push(this.getNextChunk());
    } else {
      // Signal end of readable side
      this.push(null);
    }
  },

  write(chunk, encoding, callback) {
    // Process the incoming data
    this.processChunk(chunk)
      .then(() => callback())
      .catch((err) => callback(err));
  },

  final(callback) {
    // Called when writable side is ending
    this.flushBuffers()
      .then(() => callback())
      .catch((err) => callback(err));
  }
});

Network Socket Error Handling

For TCP duplex streams (sockets), implement robust error recovery:

javascript
const net = require('net');

const client = net.createConnection({ port: 8080 });

client.on('connect', () => {
  console.log('Connected to server');
  client.write('Hello server');
});

client.on('data', (data) => {
  console.log('Received:', data.toString());
});

client.on('error', (err) => {
  console.error('Socket error:', err);
  // Don't crash, handle gracefully
});

client.on('end', () => {
  console.log('Server closed connection');
});

client.on('close', (hadError) => {
  console.log(`Socket closed, error: ${hadError}`);
  // Implement reconnection logic if needed
});

// Timeout handling
client.setTimeout(30000);
client.on('timeout', () => {
  console.log('Socket timeout');
  client.destroy(new Error('Connection timeout'));
});

Transform Stream Error Propagation

Ensure errors in transform operations are properly handled:

javascript
const { Transform } = require('stream');

class SafeTransform extends Transform {
  constructor(options) {
    super(options);
    this.errorCount = 0;
    this.maxErrors = 5;
  }

  _transform(chunk, encoding, callback) {
    try {
      const result = this.processChunk(chunk);
      callback(null, result);
    } catch (err) {
      this.errorCount++;

      if (this.errorCount >= this.maxErrors) {
        // Too many errors, destroy the stream
        callback(new Error(`Too many errors: ${err.message}`));
      } else {
        // Log but continue processing
        console.error('Transform error:', err);
        callback(); // Skip this chunk
      }
    }
  }

  _flush(callback) {
    // Called before stream ends
    if (this.errorCount > 0) {
      console.warn(`Stream completed with ${this.errorCount} errors`);
    }
    callback();
  }
}

Important Considerations

- Both sides of a duplex stream operate independently; errors on one side don't automatically propagate to the other
- Always use destroy(err) rather than end() when handling errors to ensure proper cleanup
- The 'finish' event fires when writable side completes; 'end' event fires when readable side completes
- Use 'close' event for final cleanup as it fires regardless of how the stream ended
- Be cautious with objectMode: true on one side but not the other - it can cause type errors

Frequently asked questions

What does "Error: Duplex stream ended unexpectedly (both readable and writable fa" mean?
A duplex stream error indicates that a bidirectional stream closed unexpectedly on both the readable and writable sides before data transfer completed. Duplex streams maintain two independent internal buffers for reading and writing, allowing simultaneous bidirectional communication like TCP sockets or HTTP/2 connections. When both sides of a duplex stream fail, it typically means an unhandled error propagated through the stream chain, a connection was abruptly terminated, or proper cleanup wasn't performed.
How do I fix "Error: Duplex stream ended unexpectedly (both readable and writable fa"?
This error occurs when both sides of a Node.js duplex stream (readable and writable) fail or close prematurely before completing data transfer. It commonly happens due to improper error handling, missing event listeners, or abrupt connection terminations in network streams, transform streams, or bidirectional pipes.
What causes "Error: Duplex stream ended unexpectedly (both readable and writable fa"?
Missing error event listeners on duplex or transform streams Network connection closed abruptly by client or server Unhandled exceptions in transform stream _transform() method
Is the "Error: Duplex stream ended unexpectedly (both readable and writable fa" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading