Cannot Set Headers After They Are Sent to the Client: Fix

Seeing ERR_HTTP_HEADERS_SENT in Express or a Next.js API route? Find the line that sends a second response, fix the five usual patterns, and make your error handler safe.

"Cannot set headers after they are sent to the client" (ERR_HTTP_HEADERS_SENT) means your code tried to send a second response, or change a header, after the first response had already started. Find the request path that calls res.send, res.json, res.redirect or res.end twice. Then make every path end with exactly one response, and return right after it.

The error is easy to read and hard to locate. The stack trace points at the second call, but the bug is usually the first call that did not stop the function. This guide shows how to find it, the five patterns behind most cases, and how to keep your error handler from making it worse.

Step 1: Understand what the error means

An HTTP response has two parts: the status line and headers, then the body. Node writes the headers to the socket the first time you write the body or call res.end(). After that, res.setHeader(), res.status() and res.cookie() can no longer change anything, so Node throws ERR_HTTP_HEADERS_SENT. The Node.js docs list this code among its error codes. You can reproduce it in four lines:

const http = require('node:http');

http.createServer((req, res) => {
  res.write('partial body');       // headers are sent here
  res.setHeader('X-Late', 'yes');  // throws ERR_HTTP_HEADERS_SENT
  res.end();
}).listen(3000);

In a real app you rarely write that directly. You call res.json() in two places, and one request reaches both.

Step 2: Read the stack trace from the bottom up

The first frame in your own code is the second response. Open that line, then ask one question: how did this request get here after a response was already sent?

In production, also log the route and request id with the error. A trace with no request context tells you the line but not which input triggers it. If you cannot reproduce it locally, see can't reproduce a production bug locally.

Step 3: Fix the missing return

This is the most common cause. The guard sends a response, but the function keeps running.

app.get('/users/:id', async (req, res) => {
  const user = await db.user.findUnique({ where: { id: req.params.id } });

  if (!user) {
    res.status(404).json({ error: 'Not found' }); // sent, but we keep going
  }

  res.json(user); // second response when user is null
});

Add return:

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

Several Stack Overflow answers and guides give this same fix first, because it covers most cases.

Step 4: Fix a catch block that responds twice

A try block sends the success response. Then something after it throws, and catch sends an error response.

app.post('/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);
    res.status(201).json(order);
    await sendConfirmationEmail(order); // throws after the response is sent
  } catch (err) {
    res.status(500).json({ error: 'Order failed' }); // second response
  }
});

Move work that happens after the response out of the try, or send the response last:

app.post('/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);
    await sendConfirmationEmail(order);
    return res.status(201).json(order);
  } catch (err) {
    return res.status(500).json({ error: 'Order failed' });
  }
});

If the email must not block or fail the order, run it in the background and handle its own errors. A failed side effect should not turn a paid order into a 500. For job retries, see BullMQ failed jobs.

Step 5: Fix callbacks and middleware that respond and then continue

Two more shapes show up in many threads.

A response inside a callback, plus one outside. The handler returns before the callback fires, so a response goes out twice, or the error appears on a request that was already answered.

app.get('/report', (req, res) => {
  db.query('SELECT 1', (err, rows) => {
    if (err) res.status(500).send('error'); // no return
    res.json(rows);                          // runs after the error response
  });
});

The same fix applies. Return after the error response, or move to async/await so one return ends the function.

Middleware that responds and also calls next().

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    res.status(401).json({ error: 'Unauthorized' });
  }
  next(); // the route handler now runs too and sends a second response
}

Fix it with return res.status(401)... so next() is never reached.

Step 6: Make your error handler safe

A custom error handler can create this error all by itself. If an error happens after the response started, calling res.status(500).json(...) throws again. Express documents the safe pattern: if res.headersSent is true, pass the error to next(err) and let the default handler close the connection. (We could not open the Express docs page while writing this, so check the "Error handling" guide on expressjs.com for the current wording.)

app.use((err, req, res, next) => {
  console.error({ err, path: req.path });

  if (res.headersSent) {
    return next(err);
  }

  res.status(err.status || 500).json({ error: 'Internal Server Error' });
});

Keep this handler last, after all routes, with four arguments. Our Express error handling middleware guide shows a full version with an async wrapper, so a rejected promise reaches it instead of crashing the process.

Step 7: Check the Next.js cases

In Next.js, the error mostly appears in Pages Router API routes (pages/api) and in getServerSideProps, where you get raw req and res objects. App Router route handlers return a Response, so you hit it less often, but the same rule applies: one response per request.

Two real examples from a Next.js GitHub issue:

  • A getServerSideProps function called ctx.res.end() after setting a 404 status, then returned notFound: true. Next tried to add headers to a response that had already ended. Removing the manual .end() fixed it.
  • An API route sent a response in one branch and left another branch with no response at all. Make sure every code path ends the response.
// pages/api/subscribe.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    await addSubscriber(req.body.email);
    return res.status(200).json({ ok: true });
  } catch (err) {
    return res.status(500).json({ error: 'Could not subscribe' });
  }
}

Every branch returns, and every branch responds. If you also see "API resolved without sending a response" in Next.js logs, a branch is missing its response. For other 500s on this stack, see 500 Internal Server Error in Next.js.

Step 8: Catch it in production

A double response often does no visible harm. The client gets the first response and the second throws in your logs. That makes it easy to ignore, until the second call is a database write or a payment. Treat this error as a signal that a code path runs twice.

Alert on it by name. Group by route, so you can see that POST /orders throws it and nothing else does. After you ship a fix, check that the count drops to zero rather than assuming it did.

How Relia Helps

Relia watches the users, APIs, database and servers of a live product. When a request throws ERR_HTTP_HEADERS_SENT, it joins that error with the route, the deploy that introduced it and the users who hit it, and writes it up as one incident in plain words. When there is a stack trace, it gives a fix plan with the file and line, which you or your AI assistant apply. After you deploy, it checks that the fix worked. Setup for Node.js apps is npx relia-sdk@latest init. There is a Free plan, and you can try the live demo.

FAQ

What does "Cannot set headers after they are sent to the client" mean?

Your code tried to change headers or send a body after the response had already been sent. In practice, one request produced two responses.

Is ERR_HTTP_HEADERS_SENT a problem in Next.js too?

Yes, mainly in Pages Router API routes and getServerSideProps. A manual res.end() plus a framework response, or a branch that responds twice, causes it. See the Next.js issue.

Should I just check res.headersSent everywhere? Use it in error handlers and shared middleware, where you cannot know what already ran. In route handlers, fix the cause with return. A headersSent check there hides a path that runs twice.

Why does it crash my server sometimes and not other times?

It throws inside the second call. If that call is inside a promise with no .catch(), it becomes an unhandled rejection, which can stop the process. See unhandled promise rejection in Node.js.

Does return res.send() change anything for the client? No. The client sees the same response. The return only stops your function from running further.

All guides