error nodejs

Node.js ERR_HTTP_HEADERS_SENT

Understanding Node.js ERR_HTTP_HEADERS_SENT - cannot set or send headers after they have already been sent to the client.

What It Means

ERR_HTTP_HEADERS_SENT (or “Cannot set headers after they are sent to the client”) occurs when your code tries to send a response to the client more than once for a single request. Once res.send(), res.json(), res.end(), or res.redirect() is called, the HTTP headers are sent and you cannot modify them or send another response.

Common Causes

  • Calling res.send() or res.json() multiple times
  • Not using return after sending a response in a conditional block
  • Async callback executing after a response was already sent
  • Error handler sending a response after the route already did
  • Middleware calling next() after sending a response
  • Multiple code paths sending responses without proper guards

How to Fix

Use return after sending response

// Bad: no return, so both responses execute
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);

  if (!user) {
    res.status(404).json({ error: 'Not found' });
    // Missing return! Code continues to res.json below
  }

  res.json(user); // ERR_HTTP_HEADERS_SENT!
});

// Good: return after sending response
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);

  if (!user) {
    return res.status(404).json({ error: 'Not found' });
  }

  res.json(user);
});

Fix async callback issues

// Bad: callback may fire after response already sent
app.get('/api/data', (req, res) => {
  fetchFromAPI((err, data) => {
    if (err) {
      res.status(500).json({ error: err.message });
    }
    res.json(data);  // Runs even after error response!
  });
});

// Good: use return or else
app.get('/api/data', (req, res) => {
  fetchFromAPI((err, data) => {
    if (err) {
      return res.status(500).json({ error: err.message });
    }
    res.json(data);
  });
});

Fix middleware issues

// Bad: middleware sends response AND calls next()
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    res.status(401).json({ error: 'Unauthorized' });
    next(); // Don't call next() after sending a response!
  }
  next();
});

// Good: return after sending response
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

Check if response has been sent

app.get('/api/data', async (req, res) => {
  try {
    const data = await fetchData();
    if (!res.headersSent) {
      res.json(data);
    }
  } catch (error) {
    if (!res.headersSent) {
      res.status(500).json({ error: 'Internal error' });
    }
  }
});

Fix forEach/loop with async

// Bad: multiple responses in a loop
app.get('/api/process', async (req, res) => {
  const items = [1, 2, 3];
  items.forEach(async (item) => {
    const result = await processItem(item);
    res.json(result); // Called 3 times!
  });
});

// Good: process all items, send once
app.get('/api/process', async (req, res) => {
  const items = [1, 2, 3];
  const results = await Promise.all(items.map(processItem));
  res.json(results); // Called once
});

Proper error handling pattern

// Wrap async routes to catch errors
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/api/data', asyncHandler(async (req, res) => {
  const data = await getData();
  res.json(data);
}));

// Global error handler (only one response)
app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err); // Delegate to Express default handler
  }
  res.status(500).json({ error: 'Something went wrong' });
});