Stop Express.js from Crashing: Fixing 'Error [ERR_HTTP_HEADERS_SENT]'

intermediate๐Ÿ’š Node.js2026-04-23| Node.js (all versions), Express.js, Linux/macOS/Windows

Error Message

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
#nodejs#express#http#debugging#backend

The Error Message

Your terminal is probably screaming at you with a stack trace that looks like this:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:371:5)
    at ServerResponse.setHeader (node:_http_outgoing:576:11)
    at ServerResponse.header (/project/node_modules/express/lib/response.js:771:10)
    at ServerResponse.send (/project/node_modules/express/lib/response.js:170:12)

The Root Cause: One Request, One Response

Think of an HTTP request like a single-use ticket. Once you use it to "buy" a response, it's gone. In the HTTP/1.1 protocol, the relationship between a request and a response is strictly 1:1. As soon as you call res.send(), res.json(), or res.end(), Express locks the headers and ships them to the browser.

Crucially, calling a response method doesn't kill your function. JavaScript is happy to keep running every line of code after res.send() until it hits a return or the end of the block. If that remaining code tries to send another response, Node.js throws this error to prevent a protocol violation.

Common Scenarios and Fixes

1. The "Missing Return" Trap

This is easily the most common mistake. Developers often forget that res.send() isn't a return statement. If your validation logic fails, the code might send an error but then keep right on going to the success logic.

The Buggy Code:

app.post('/login', (req, res) => {
  const { username } = req.body;

  if (!username) {
    res.status(400).json({ error: 'Missing username' });
    // The function keeps running! 
  }

  // This triggers the crash because headers were already sent above
  res.status(200).json({ message: 'Welcome!' }); 
});

The Fix:

Always prepend your response calls with return. It's a simple habit that saves hours of debugging.

app.post('/login', (req, res) => {
  const { username } = req.body;

  if (!username) {
    return res.status(400).json({ error: 'Username is required' });
  }

  return res.status(200).json({ message: 'Success' });
});

2. Responses Hidden in Loops

Iterating over a list and sending a response inside the loop is a recipe for disaster. The first item will send successfully. The second item will crash your process instantly.

The Buggy Code:

app.get('/search', (req, res) => {
  const items = [1, 2, 3];
  items.forEach(item => {
    if (item === 2) {
      res.send('Found it!'); // Works for item 2, but what about item 3?
    }
  });
});

The Fix:

Process your logic entirely before touching the res object. Find your data first, then send it once.

app.get('/search', (req, res) => {
  const items = [1, 2, 3];
  const match = items.find(i => i === 2);
  
  if (match) {
    return res.send('Found it!');
  }
  return res.status(404).send('Not Found');
});

3. Async Callback Overlap

Database queries or API calls often use callbacks. If your error handling doesn't stop execution, you might accidentally trigger both an error response and a success response.

The Buggy Code:

app.get('/profile', (req, res) => {
  User.findById(id, (err, user) => {
    if (err) {
       res.status(500).send('Database failure');
       // No return here means the code keeps moving...
    }
    res.json(user); // Crashing here if err existed!
  });
});

The Fix:

Modernize your code with async/await. It makes the execution path much easier to read and control with try/catch blocks.

app.get('/profile', async (req, res) => {
  try {
    const user = await User.findById(id);
    return res.json(user);
  } catch (err) {
    return res.status(500).send('Database failure');
  }
});

Step-by-Step Debugging Strategy

  • Pinpoint the double-call: Check the stack trace. It usually points to the second res.send() call that failed, not the first one that succeeded.
  • Audit your returns: Search your file for res. and ensure every branch of your logic ends with a return or an else.
  • Check Middleware: Ensure your custom middleware isn't calling next() after it has already sent a response. That's a common source of "ghost" crashes.
  • Monitor Logs: Use a tool like morgan to see exactly which responses are being sent before the crash occurs.

Pro-Tips for Clean Code

  • Adopt the "Return res" Pattern: Make return res.json(...) your default syntax. It's safer and clearer.
  • Use ESLint: Enable the consistent-return rule. It will highlight functions where you might have missed a return path.
  • Guard Rails: In very complex logic, you can check if (res.headersSent) return;. However, treat this as a last resort; it's usually a sign that your function is doing too much and needs a refactor.

Related Error Notes