Solving 'Error: write after end' in Node.js Streams

intermediate💚 Node.js2026-04-03| Node.js (v12.9.0+ recommended for writableEnded), occurring across all OS environments during stream or HTTP operations.

Error Message

Error: write after end
#nodejs#streams#expressjs#backend#error-handling

The 30-Second Solution

This error is Node's way of telling you that you're trying to send data to a stream that has already been closed. It is like trying to mail a letter after the post office has locked its doors for the night. This usually happens because .end() was called prematurely or multiple times. To stop the crash immediately, verify the stream state before writing:

if (!myStream.writableEnded) {
  myStream.write(data);
}

If you are using .pipe(), stop calling .end() manually on the destination. The pipe mechanism handles that transition for you.

Why This Happens

Every Writable Stream in Node.js follows a strict lifecycle. Once you signal that no more data is coming by calling .end(), the stream enters a finalized state. Any attempt to use .write() or call .end() again after this point triggers the exception. In a high-concurrency environment—say, handling 1,000 requests per second—these errors often point to logic gaps in asynchronous code.

Where things usually go wrong:

  • Double Response in Express: You might trigger res.json() but let the code execution fall through to another res.send() later in the same function.
  • The Async Race: A database query or an external API call returns after the client has already timed out or the request has been closed.
  • Manual Pipe Interference: You manually close a file stream while fs.createReadStream().pipe(dest) is still trying to push chunks.
  • Error Handler Loops: An error occurs, your handler closes the stream, but the original logic tries to finish its current write operation anyway.

Practical Fixes

1. Guarding Your Writes

Checking the writableEnded property is your first line of defense. It is especially useful when dealing with unpredictable third-party streams or complex event emitters.

function safelyWrite(stream, data) {
  // writableEnded was introduced in Node v12.9.0
  if (stream.writable && !stream.writableEnded) {
    stream.write(data);
  } else {
    console.warn("Prevented a write attempt to a closed stream");
  }
}

2. Switch to pipeline()

The classic source.pipe(dest) is often a memory leak waiting to happen because it doesn't destroy the rest of the chain if one stream fails. The stream.pipeline utility is a much safer alternative. It manages the cleanup for you and ensures that if the destination closes, the source stops immediately.

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

// This properly handles errors and prevents orphan writes
pipeline(
  fs.createReadStream('large-archive.tar'),
  zlib.createGzip(),
  fs.createWriteStream('large-archive.tar.gz'),
  (err) => {
    if (err) {
      console.error('Pipeline failed:', err);
    } else {
      console.log('Compression complete');
    }
  }
);

3. Fix Express 'Return' Logic

In Express, res.send() and res.json() automatically call res.end(). If you don't use the return keyword, the rest of your function will still run, leading to a second write attempt.

// THE WRONG WAY
app.get('/user/:id', (req, res) => {
  if (!req.params.id) {
    res.status(400).send('ID required');
  }
  res.send('User Data'); // CRASH: write after end if ID was missing
});

// THE RIGHT WAY
app.get('/user/:id', (req, res) => {
  if (!req.params.id) {
    return res.status(400).send('ID required'); // 'return' exits the function
  }
  res.send('User Data');
});

4. Guarding Async Callbacks

When working with database results, always check if the client is still listening. If a query takes 5 seconds but the user refreshes their browser after 2 seconds, the stream will already be closed when the data arrives.

db.users.find({ id }, (err, user) => {
  if (res.writableEnded) return; // The user left; don't try to send data

  if (err) return res.status(500).send(err);
  res.json(user);
});

How to Verify the Fix

Don't just assume it's fixed. Test these scenarios:

  • Simulate Client Aborts: Use a tool like curl and hit Ctrl+C mid-request to see if your server logs an unhandled exception.
  • Stress Testing: Run autocannon -c 100 -d 10 http://localhost:3000/data. High load often exposes race conditions that don't appear in local dev.
  • Check Stream State: Use console.log('Finished:', stream.writableFinished) to trace exactly where your stream's life ends.

Resources

Related Error Notes