All posts
nodejshttp

Fixing "Cannot set headers after they are sent to the client"

Why Node.js throws Cannot set headers after they are sent, common patterns that cause it, and how to fix double response handling.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

Error: Cannot set headers after they are sent to the client means your Node.js server tried to send a response twice for the same request — a genuinely common bug in request handlers with async logic or multiple possible code paths, since it's easy to accidentally call res.send() or res.json() more than once without realizing both paths executed.

This error means your request handler attempted to write a response (or set a header) after a response had already been sent for that request — HTTP responses can only be sent once per request, and Node's HTTP module enforces this strictly, throwing rather than silently ignoring the second attempt.

Why This Error Happens

Once res.end() (called internally by res.send(), res.json(), etc.) has been invoked, the response is considered complete and headers are already flushed to the client — any subsequent attempt to set a header or send a body fails. This most commonly happens when a handler has multiple exit paths that aren't mutually exclusive, missing return statements after sending a response, or async code that runs after a response was already sent for an earlier, unrelated reason (like an error path).

Reproducing the Error

A missing return statement after an early response:

app.get("/api/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) {
    res.status(404).json({ error: "Not found" }); // sends response
    // missing return — execution continues below
  }
  res.json(user); // throws: Cannot set headers after they are sent
  // (also crashes trying to access properties on `user`, which is null here)
});

Or an async callback firing after the response was already sent via a timeout or error path:

app.get("/api/data", (req, res) => {
  const timeout = setTimeout(() => {
    res.status(504).json({ error: "Timeout" });
  }, 5000);

  fetchData().then((data) => {
    clearTimeout(timeout);
    res.json(data); // if this runs after the timeout already fired, throws
  });
});

Core Concepts Behind This Error

Every code path in a request handler that can send a response needs an explicit return immediately after doing so, to prevent execution from continuing to a later response-sending statement — this is the single most common cause and the simplest to fix once you know to look for it.

Async handlers with multiple possible completion triggers (a timeout, a callback, an error handler) need explicit coordination to ensure only one of them actually sends the response — a race between a timeout and a successful async operation, both trying to respond, is a classic version of this bug.

Error-handling middleware that runs after a response was already sent silently by an earlier handler is a subtler variant — if a preceding middleware or route handler already sent a response but didn't halt further middleware execution, a later error handler attempting to send its own response will hit this error.

This error doesn't crash the whole process by default in most setups, but it does indicate a genuine logic bug — the client only receives the first response sent, and the failed second attempt represents dead, broken code that should be fixed even though the user might not visibly notice.

Fixing "Cannot Set Headers After They Are Sent"

Fix 1: Add explicit return statements after every response-sending call in a handler with multiple exit paths:

app.get("/api/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: "Not found" }); // return prevents fallthrough
  }
  res.json(user);
});

Fix 2: Coordinate competing async completion paths with a guard flag or by canceling the alternative path explicitly:

app.get("/api/data", (req, res) => {
  let responded = false;
  const timeout = setTimeout(() => {
    if (!responded) {
      responded = true;
      res.status(504).json({ error: "Timeout" });
    }
  }, 5000);

  fetchData().then((data) => {
    if (!responded) {
      responded = true;
      clearTimeout(timeout);
      res.json(data);
    }
  });
});

Fix 3: Check res.headersSent before attempting to respond in error-handling code, particularly in middleware that might run after a response was already dispatched by an earlier handler:

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (res.headersSent) {
    return next(err); // delegate to default handler rather than trying to respond again
  }
  res.status(500).json({ error: "Internal Server Error" });
});

Why Doesn't Node.js Just Silently Ignore the Second Response Attempt?

Because silently ignoring it would hide a real logic bug — the second res.send() call represents a code path that ran unexpectedly, and if Node silently no-op'd it, you'd have no signal that your handler's control flow doesn't work the way you assumed. The explicit throw is deliberately noisy specifically so this class of bug gets caught during development rather than manifesting later as a confusing, hard-to-diagnose issue.

Preventing This Error in Production

Adopt a consistent pattern of return-ing immediately after every response-sending call in handlers with conditional logic, treating a missing return after res.send()/res.json() as a lint-worthy issue. For handlers with multiple async completion paths (timeouts racing against actual results), use an explicit guard to ensure only the first to complete actually sends a response, and cancel or ignore the other.

If you hit this error, look for the handler's second response-sending call and trace backward for a missing return or an uncoordinated async race — it's almost always one of those two patterns.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch