Fix 'ERR_STREAM_DESTROYED: Cannot call write after a stream was destroyed' in Node.js

intermediate๐Ÿ’š Node.js2026-05-10| Node.js 10+, all platforms (Linux, macOS, Windows)

Error Message

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
#nodejs#stream#writable-stream#pipe#destroy

The Error

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed

You tried to write to a stream that's already gone. Maybe a client disconnected mid-response. Maybe an async database call came back 200ms too late โ€” after res.end() had already fired. Node.js sets an internal destroyed flag and refuses any further writes, throwing this as an unhandled exception or an error event on the stream.

Root Cause

Streams in Node.js have a strict lifecycle. Once stream.destroy() is called โ€” explicitly by your code, or automatically when an HTTP response ends or a socket drops โ€” the destroyed flag flips to true. That's permanent. Any .write() after that point throws this error.

The usual suspects:

  • Writing to an HTTP response after res.end() or a client disconnect
  • An async callback (database query, external API call) resolving after the stream already closed
  • A piped readable stream pushing data after the writable destination was destroyed
  • Reusing a stream object after it was destroyed

Fix 1 โ€” Check destroyed Before Writing

The bluntest fix: check the flag before you write.

function safeWrite(stream, chunk) {
  if (stream.destroyed) {
    return; // silently skip โ€” stream is gone
  }
  stream.write(chunk);
}

For HTTP responses, two flags matter:

app.get('/data', async (req, res) => {
  const data = await fetchSomething();

  if (res.destroyed || res.writableEnded) {
    return; // client disconnected or res.end() already called
  }

  res.json(data);
});

res.writableEnded becomes true the moment res.end() is called. res.destroyed becomes true once the underlying socket is gone. Both cases are fatal โ€” check both.

Fix 2 โ€” Handle the close Event to Cancel Async Work

Got a route that streams rows from a database? A client that disconnects after receiving 50 of 5,000 rows will trigger this error if your loop keeps writing. Set a cancellation flag on the close event and bail out early.

app.get('/stream-data', (req, res) => {
  let cancelled = false;

  res.on('close', () => {
    cancelled = true; // client disconnected
  });

  async function streamRows() {
    for await (const row of getDatabaseRows()) {
      if (cancelled) break;
      res.write(JSON.stringify(row) + '\n');
    }
    if (!cancelled) res.end();
  }

  streamRows().catch(err => {
    if (!cancelled) res.destroy(err);
  });
});

Fix 3 โ€” Use AbortController with Pipelines

Piping streams? Pass an AbortSignal to stream.pipeline() so a disconnect aborts the whole chain cleanly โ€” no dangling write attempts.

const { pipeline } = require('stream/promises');
// AbortController is built-in from Node.js 16+

const ac = new AbortController();

req.on('close', () => ac.abort()); // client disconnected โ†’ abort

try {
  await pipeline(sourceStream, transformStream, res, { signal: ac.signal });
} catch (err) {
  if (err.name !== 'AbortError') {
    console.error('Pipeline failed:', err);
  }
}

stream.pipeline() destroys every stream in the chain on error or abort. No manual cleanup needed.

Fix 4 โ€” Catch the Error Event on the Stream

Sometimes you can't guard every write path. At minimum, attach an error handler so the error doesn't crash your entire process.

const writable = getWritableStream();

writable.on('error', (err) => {
  if (err.code === 'ERR_STREAM_DESTROYED') {
    // expected โ€” stream closed before all data was written
    return;
  }
  console.error('Unexpected stream error:', err);
});

This is a safety net. It won't stop the error from happening โ€” it just stops it from taking down your server.

Fix 5 โ€” Don't Destroy a Stream You're Still Writing To

If you control when the stream closes, let it drain first.

// Wrong โ€” destroys immediately, pending writes fail
stream.write(largeChunk);
stream.destroy();

// Correct โ€” end() flushes pending writes then closes
stream.write(largeChunk);
stream.end(); // waits for write to complete, then closes gracefully

Use stream.end() for a clean shutdown. Save stream.destroy() for error paths where you need to force-close without flushing โ€” like aborting a large upload gone wrong.

Verification

After your fix, simulate the failure to confirm it's gone:

  • Start your server and hit a streaming endpoint. Disconnect mid-response โ€” cancel a fetch in the browser, or run curl and press Ctrl+C while it's streaming
  • Check your logs. ERR_STREAM_DESTROYED should be absent
  • For CI, add a test that destroys the stream mid-pipeline and asserts no unhandled errors:
it('does not throw when stream is destroyed mid-write', (done) => {
  const writable = new PassThrough();
  writable.on('error', done); // fail test on unexpected error

  writable.write('first chunk');
  writable.destroy();

  // safeWrite should swallow ERR_STREAM_DESTROYED silently
  expect(() => safeWrite(writable, 'second chunk')).not.toThrow();
  done();
});

Prevention

  • Prefer stream.pipeline() or .pipe() over manual write loops โ€” teardown is automatic
  • Listen for the close event on HTTP responses to catch client disconnects early
  • Always check stream.destroyed or res.writableEnded before any async write
  • Reach for stream.end() first; only use stream.destroy() when you need a forced, unflushed close
  • On Node.js 16+, wire up AbortController to stream.pipeline() for cancellable pipelines

Related Error Notes