ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent (tested)

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client means your code tried to respond twice to one request. The first response already went out. We reproduced seven common causes in Express 5.2.1 on Node 24 and measured what the server logs, what the client receives, and when the whole process dies.

1. What the error means

An HTTP response has one status line and one set of headers. Once Node has sent them, they cannot change. Any later call that sets a header throws this error. In Express that includes res.send(), res.json(), res.status().send(), res.redirect() and res.setHeader().

The top of the stack is always the same. This is the exact output we got from a second res.send():

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (node:_http_outgoing:684:11)
    at ServerResponse.header (.../express/lib/response.js:686:10)
    at ServerResponse.send (.../express/lib/response.js:163:12)
    at /tmp/gheaderssent/app.js:9:7

The first line under express/lib/response.js that points into your file is the second response. That is the line to look at. The first response happened earlier and is not in the stack.

2. Results: seven ways to trigger it

CauseClient receivedServer
Missing return after res.status(400).send()400 "name required", completeStack logged, process kept running
res.json() inside forEach200 {"i":1} onlyStack logged once (the loop stops at the throw)
Callback and promise both respond200 "promise-data"Process crashed, exit code 1
next() after res.send()200 "done"Stack logged from the next handler's res.send
res.setHeader() after res.write()Nothing: curl (52) Empty reply from server; fetch "fetch failed", cause UND_ERR_SOCKET "other side closed"Stack logged, socket destroyed
res.redirect() after res.send()200 "ok", no redirectStack logged, top frames res.location / res.redirect
Second res.send() after await in an async handler200 "ok"Stack logged, process kept running

Most of the time the client gets the first response and never sees an error. That is why this bug hides in production: users get a 200 or 400 and only your logs show the stack.

3. Does it crash the process?

It depends on where the second response is called from. We ran each case with no process.on('uncaughtException') handler:

When Express does catch it, its default error handler (finalhandler 2.1.1) prints the stack with console.error and then, because headers were already sent, calls req.socket.destroy(). If the first response was complete, the client is fine. If it was a half-written stream, the client gets a cut-off or empty reply. That is what happened in the res.write() case: the buffered "part" was never flushed, so curl got an empty reply.

4. Fix: return when you respond

The most common cause is a guard clause without return:

// Bug: falls through to the second res.send
app.get('/hello', (req, res) => {
  if (!req.query.name) { res.status(400).send('name required'); }
  res.send('hello ' + req.query.name);
});

// Fix
app.get('/hello', (req, res) => {
  if (!req.query.name) { return res.status(400).send('name required'); }
  res.send('hello ' + req.query.name);
});

Verified: the fixed route returned 400 without a name and 200 "hello a" with one, and logged nothing.

Apply the same rule to if/else chains, try/catch (a catch that responds after the try already did), and middleware: once you send, do not call next(). Our next-fixed route (send, no next()) logged nothing.

5. Fix: one response per request

6. Guard with res.headersSent

res.headersSent is true once the status line and headers are on the wire. Use it when a late path really cannot know if someone else already answered, such as a timeout handler racing the real work:

app.get('/report', async (req, res) => {
  res.send('ok');
  await somethingSlow();
  if (!res.headersSent) res.send('again');   // skipped
});

Verified: the guard logged headersSent= true and skipped the second send. No error.

Do not use it to paper over a real double response. A missing return is better fixed with a return.

Custom error handlers need the same check. Express's docs say to delegate to the default handler when headers are already sent:

app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  res.status(500).send('error');
});

Without that check, your handler's own res.status(500).send() throws a second ERR_HTTP_HEADERS_SENT. With it, our handler saw headersSent= true for every case above and handed off cleanly.

7. Errors thrown after the response

An error that happens after you already answered is not ERR_HTTP_HEADERS_SENT, but it lands in the same place. We sent "ok", awaited 10 ms, then threw new Error('db write failed'). The client got 200 "ok". Express 5 forwarded the error, our error handler saw headersSent= true, and the default handler logged Error: db write failed. The process kept running. The client never learns the write failed, so do slow work before you respond, or use a queue.

If the client gets a 500 instead, see 500 Internal Server Error. If Express answers Cannot GET /path, the route did not match at all: see Cannot GET in Express.

8. Checklist

Frequently asked questions

Why does the client get a normal response when the server logs this error?

Because the first response already went out complete. In our tests the client got the first 400 or 200 every time the first response was a full res.send(). Only the second call fails, and only the server sees it.

Does ERR_HTTP_HEADERS_SENT crash Node?

Not in a sync or async Express 5 route: Express catches it and logs the stack. It did crash our process (exit code 1) when the second send ran in a setTimeout callback or in a .then() chain the handler did not return.

How do I find which line sends the second response?

Look at the stack below express/lib/response.js. The first frame in your own file is the second res.send, res.json or res.redirect. The first response is earlier in the same request path.

Should I check res.headersSent before every send?

No. Fix the control flow with return first. Use res.headersSent where two paths race, like a timeout and the real work, and always in custom error handlers: if (res.headersSent) return next(err).

Why did my client get an empty reply?

If headers or part of a body were already written when the error happened, Express's default handler destroys the socket. In our res.write() test curl reported (52) Empty reply from server and fetch failed with UND_ERR_SOCKET other side closed.

Written with AI assistance. Measured on 2026-10-02 with Express 5.2.1 (finalhandler 2.1.1), Node.js 24.21.0, curl 8.14.1 and Node fetch. Every bug and every fix below was run as a real route and hit with curl. PlainHeaders is our extension; Express, Node.js and curl are free.