Node.js Error

ERR_HTTP_HEADERS_SENT

error code: ERR_HTTP_HEADERS_SENT

Complete reference for Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client — every cause, the res.headersSent guard pattern, and full code examples for Express, Fastify, and raw http.createServer.

Quick Answer: Node.js throws ERR_HTTP_HEADERS_SENT when your code calls res.send(), res.json(), res.redirect(), res.setHeader(), or res.end() a second time on the same request. The most common fix is adding return before every response call (return res.json({...})) so execution stops immediately. Also check for double next() calls in middleware and async callbacks that fire after the response was already sent.

What is ERR_HTTP_HEADERS_SENT?

ERR_HTTP_HEADERS_SENT is thrown by Node.js's HTTP internals when a response has already been written to the TCP socket and code subsequently tries to modify headers or write more data. HTTP responses are strictly one-way and one-time: once the status line and headers are flushed, the response is sealed. Any call to res.setHeader(), res.writeHead(), res.write(), or res.end() on an already-finished response triggers this error.

In Express, res.send(), res.json(), res.render(), and res.redirect() all call res.end() internally, sealing the response. In Fastify, reply.send() does the same. In raw http.createServer, res.end() or res.write() after a prior res.end() will trigger it.

See the official Node.js error documentation for the internal definition.

Full Error Example

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:405:5)
    at ServerResponse.setHeader (node:_http_outgoing:644:11)
    at ServerResponse.header (/app/node_modules/express/lib/response.js:794:10)
    at ServerResponse.json (/app/node_modules/express/lib/response.js:278:10)
    at /app/src/routes/users.js:24:9       <-- second res.json() call
    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
    at next (/app/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
    at /app/src/routes/users.js:18:5       <-- first res.json() call succeeded here
  code: 'ERR_HTTP_HEADERS_SENT'

The stack trace always shows two lines in your own code: the line where the second response call was made (the one that threw) and the line where the first succeeded. Start debugging at your own file lines, not the Express or Node.js internals.

Error Object Properties

PropertyValueNotes
error.code'ERR_HTTP_HEADERS_SENT'Always this exact string
error.message'Cannot set headers after they are sent to the client'Consistent across Node.js versions
error.name'Error'Base Error class, not TypeError
res.headersSenttrueCheck this before any response call in defensive code

All Causes at a Glance

Cause Symptom Fix
Missing return before response call Two res.json() / res.send() calls in one handler return res.json({...})
Missing return after next() Middleware continues executing after calling next() return next()
next(err) after already responding catch block calls both res.send() and next(err) Use only next(err) in catch; never send and forward
Async callback fires after response setTimeout, Promise.then, DB callback, event fires late Guard with if (res.headersSent) return;
Double next() call in middleware next() called twice in same middleware function return next() on every call path
res.setHeader() after res.end() Raw http module header set too late Set all headers before writing any body data
Fastify reply.send() called twice Async handler returns a value AND calls reply.send() Return value only; do not also call reply.send()
Express error middleware sends then calls next() Error handler sends a response and also calls next(err) Error middleware must send a response OR call next(), never both

Cause 1 – Missing return Before res.send() (Most Common)

res.send(), res.json(), and res.redirect() do not stop the execution of your route handler function. They are regular JavaScript function calls. Without a return statement, code after the response call continues running and hits another response call.

// BROKEN – missing return; execution falls through to the second res.json()
app.post('/users', async (req, res) => {
  if (!req.body.email) {
    res.status(400).json({ error: 'Email is required' }); // sends response
    // No return here — code continues!
  }
  // This line runs even when the 400 was already sent:
  const user = await createUser(req.body);
  res.status(201).json(user); // Error [ERR_HTTP_HEADERS_SENT]
});

// FIXED – return before every response call
app.post('/users', async (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({ error: 'Email is required' }); // stops here
  }
  const user = await createUser(req.body);
  return res.status(201).json(user);
});
Rule of thumb: prefix every res.* call with return. It costs nothing and prevents the entire class of "double response" bugs. ESLint's consistent-return rule and TypeScript's return-type inference can enforce this automatically.

Cause 2 – Double next() Calls in Middleware

Express middleware must call next() exactly once, or send a response — never both, and never next() twice. Without a return after next(), the middleware continues executing and may call next() again, passing the same request to two subsequent route handlers.

// BROKEN – double next() call
function authMiddleware(req, res, next) {
  if (!req.headers.authorization) {
    next(new Error('Unauthorized')); // passes to error handler
    // No return — code continues!
  }
  // Called again for authenticated requests AND for unauthorized ones:
  next(); // Error [ERR_HTTP_HEADERS_SENT] when the error handler also responds
}

// FIXED – return after every next() call
function authMiddleware(req, res, next) {
  if (!req.headers.authorization) {
    return next(new Error('Unauthorized')); // stops here
  }
  return next(); // only reached for authorized requests
}

// ALSO BROKEN – calling next() and then responding
function loggingMiddleware(req, res, next) {
  next(); // passes control to next handler which sends a response
  res.setHeader('X-Request-Id', generateId()); // response already sent!

  // FIXED – set headers BEFORE calling next()
  // res.setHeader('X-Request-Id', generateId());
  // next();
}

Cause 3 – Calling next(err) After Already Responding

A common pattern is to wrap an async handler in a try/catch. The catch block should either send an error response or call next(err) to delegate to Express's error middleware — never both. Doing both sends one response in the catch block and then the error middleware tries to send another.

// BROKEN – sends a response AND forwards to error middleware
app.get('/data', async (req, res, next) => {
  try {
    const result = await fetchData();
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: err.message }); // sends response
    next(err); // error middleware tries to send ANOTHER response
  }
});

// FIXED – use only next(err) in catch; let error middleware handle the response
app.get('/data', async (req, res, next) => {
  try {
    const result = await fetchData();
    res.json(result);
  } catch (err) {
    next(err); // delegate completely to error middleware
  }
});

// Error middleware (registered last):
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err); // safety guard
  res.status(err.status || 500).json({ error: err.message });
});

Cause 4 – Async Callback Fires After Response is Finished

Any code that runs asynchronously — a setTimeout, a database callback, a Promise.then(), or an event emitter listener — may execute after the response was already sent by a different code path (a timeout, an error handler, or an earlier branch). The fix is to guard every async callback with if (res.headersSent) return;.

// BROKEN – setTimeout fires after the response was already sent
app.get('/slow', (req, res) => {
  // Request-level timeout sends a response after 5 seconds
  setTimeout(() => {
    res.status(503).json({ error: 'Timeout' });
  }, 5000);

  // If fetchData() resolves in 3 seconds, this sends first:
  fetchData().then(data => {
    res.json(data); // sends after 3s
    // 2 seconds later, the setTimeout fires and tries to send again — ERROR
  });
});

// FIXED – guard the async callback with res.headersSent
app.get('/slow', (req, res) => {
  const timer = setTimeout(() => {
    if (res.headersSent) return; // data already came back — do nothing
    res.status(503).json({ error: 'Request timed out' });
  }, 5000);

  fetchData()
    .then(data => {
      clearTimeout(timer); // cancel the timeout since we have a result
      if (res.headersSent) return;
      res.json(data);
    })
    .catch(err => {
      clearTimeout(timer);
      if (res.headersSent) return;
      res.status(500).json({ error: err.message });
    });
});

// BEST – use async/await with AbortController for clean timeout handling
app.get('/slow', async (req, res) => {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const data = await fetchData({ signal: controller.signal });
    clearTimeout(timer);
    res.json(data);
  } catch (err) {
    clearTimeout(timer);
    if (err.name === 'AbortError') {
      return res.status(503).json({ error: 'Request timed out' });
    }
    res.status(500).json({ error: err.message });
  }
});

Cause 5 – Async Middleware Calling next() After Response

When async middleware awaits something and then calls next(), the subsequent route handler may already have sent a response (e.g. if another middleware errored). Always check res.headersSent before calling next() in async middleware.

// BROKEN – async middleware calls next() after awaiting; response may already be sent
async function rateLimitMiddleware(req, res, next) {
  const allowed = await checkRateLimit(req.ip); // slow DB check
  if (!allowed) {
    return res.status(429).json({ error: 'Too Many Requests' });
  }
  next(); // by the time this runs, another middleware may have sent a 500
}

// FIXED – guard next() with headersSent check
async function rateLimitMiddleware(req, res, next) {
  try {
    const allowed = await checkRateLimit(req.ip);
    if (res.headersSent) return; // another middleware already responded
    if (!allowed) {
      return res.status(429).json({ error: 'Too Many Requests' });
    }
    next();
  } catch (err) {
    if (!res.headersSent) next(err);
  }
}

Cause 6 – Fastify: reply.send() Called Twice

Fastify's async handlers automatically send the return value as the response body. If you also call reply.send() manually inside an async handler that returns a value, Fastify throws the equivalent of ERR_HTTP_HEADERS_SENT. Fastify exposes reply.sent as the guard property.

// BROKEN in Fastify – returns a value AND calls reply.send()
fastify.get('/user/:id', async (request, reply) => {
  const user = await getUser(request.params.id);
  reply.send(user); // sends the response
  return user;      // Fastify tries to send again — Error: Reply already sent
});

// FIXED – choose one: return the value OR call reply.send(), not both
fastify.get('/user/:id', async (request, reply) => {
  const user = await getUser(request.params.id);
  return user; // Fastify sends this automatically
});

// Alternative – use reply.send() and return nothing (or return reply)
fastify.get('/user/:id', async (request, reply) => {
  const user = await getUser(request.params.id);
  reply.send(user);
  // return nothing — Fastify sees reply.sent = true and skips auto-send
});

// Guard pattern in Fastify for defensive async code:
fastify.get('/stream', async (request, reply) => {
  try {
    const data = await slowOperation();
    if (reply.sent) return; // already responded (e.g. request was aborted)
    return data;
  } catch (err) {
    if (!reply.sent) reply.status(500).send({ error: err.message });
  }
});

Cause 7 – Raw http.createServer: Headers Set After res.end()

In raw Node.js HTTP handlers (no Express), headers must be set before any call to res.write() or res.end(). Calling res.setHeader() or res.writeHead() after the response body has been written triggers the error.

const http = require('http');

// BROKEN – setHeader after end
const server = http.createServer((req, res) => {
  res.end('Hello World'); // flushes headers + body
  res.setHeader('X-Custom', 'value'); // Error [ERR_HTTP_HEADERS_SENT]
});

// FIXED – set all headers before write/end
const server2 = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/plain');
  res.setHeader('X-Custom', 'value');
  res.writeHead(200); // optional; writeHead must come before write/end too
  res.end('Hello World');
});

// BROKEN – writeHead after implicit header flush via res.write()
const server3 = http.createServer((req, res) => {
  res.write('chunk one'); // this implicitly sends headers with status 200
  res.writeHead(404);     // Error [ERR_HTTP_HEADERS_SENT] — too late
  res.end();
});

// FIXED – write status in writeHead or statusCode before any write()
const server4 = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('chunk one');
  res.write('chunk two');
  res.end();
});

The res.headersSent Guard Pattern

res.headersSent is a built-in boolean property on Node.js's ServerResponse object (and on Express's Response wrapper). It is false before any response data is sent and true immediately after. Use it as the primary defensive guard anywhere code might try to respond twice.

// Universal guard — works in Express, raw http, and any framework built on Node.js http
function safeRespond(res, statusCode, body) {
  if (res.headersSent) {
    console.warn('safeRespond: response already sent, skipping', body);
    return;
  }
  res.status(statusCode).json(body);
}

// Express error middleware with headersSent guard (required pattern)
app.use((err, req, res, next) => {
  if (res.headersSent) {
    // Response already started — just close the connection
    return next(err);
  }
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});

// Async route with multiple failure points
app.get('/report', async (req, res, next) => {
  try {
    const [users, orders] = await Promise.all([
      getUsers(),
      getOrders()
    ]);
    if (res.headersSent) return;
    res.json({ users, orders });
  } catch (err) {
    if (!res.headersSent) next(err);
  }
});

Safe Express Patterns

Pattern A – return on every branch

app.put('/item/:id', async (req, res, next) => {
  const { id } = req.params;

  if (!id || isNaN(Number(id))) {
    return res.status(400).json({ error: 'Invalid id' });
  }

  let item;
  try {
    item = await db.findById(id);
  } catch (err) {
    return next(err); // delegate to error middleware
  }

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

  try {
    const updated = await db.update(id, req.body);
    return res.json(updated);
  } catch (err) {
    return next(err);
  }
});

Pattern B – single return point via async/await + centralized error handling

// Wrap all routes with an async error-catching wrapper
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.put('/item/:id', asyncHandler(async (req, res) => {
  const id = Number(req.params.id);
  if (!id) return res.status(400).json({ error: 'Invalid id' });

  const item = await db.findById(id);
  if (!item) return res.status(404).json({ error: 'Not found' });

  const updated = await db.update(id, req.body);
  res.json(updated);
}));
// Any thrown error is caught by .catch(next) and forwarded to error middleware

Pattern C – express-async-errors package (zero boilerplate)

// Install: npm install express-async-errors
// Add this import once at the top of your app entry point:
require('express-async-errors');

// After this, async errors are automatically forwarded to next() — no try/catch needed
app.get('/data', async (req, res) => {
  const data = await fetchData(); // if this throws, Express catches it automatically
  res.json(data);
});

Debugging Checklist

  1. Read the stack trace — find the two lines in your own code: the first response call (which succeeded) and the second (which threw). Start there.
  2. Add return before every res.send(), res.json(), res.redirect(), and res.render() call in the affected route handler.
  3. Add return after every next() and next(err) call in middleware functions.
  4. Audit every catch block — confirm it either calls next(err) or sends a response, never both.
  5. Search for setTimeout, .then(, event listeners, and database callbacks inside route handlers — each one must start with if (res.headersSent) return;.
  6. Check error middleware — it must guard with if (res.headersSent) return next(err); as the very first line.
  7. Temporarily add console.trace('response sent') inside each response call to capture the full call stack in the logs and identify which code path fires first.
  8. For Fastify: confirm async route handlers either return the value or call reply.send() — not both.
  9. For raw http.createServer: confirm all res.setHeader() and res.writeHead() calls appear before any res.write() or res.end().
  10. Run the route under load (e.g. with autocannon or k6) to expose timing-dependent bugs that only appear under concurrent requests.
Common mistake — swallowing the error: Wrapping the entire handler in a try/catch that silently ignores the ERR_HTTP_HEADERS_SENT error hides the bug. The underlying double-send still happened — one of the two responses was silently dropped. Fix the root cause (missing return or a race condition) rather than swallowing the error.

Frequently Asked Questions

What is ERR_HTTP_HEADERS_SENT?

ERR_HTTP_HEADERS_SENT is a Node.js error thrown when code attempts to send HTTP headers or a response body after the HTTP response has already been sent to the client. Once res.end() is called (directly or via res.send(), res.json(), res.redirect(), or res.render() in Express), the response is sealed. Any further attempt to write to it triggers this error.

What causes ERR_HTTP_HEADERS_SENT?

The most common causes are:

  • Missing return before res.send() / res.json() in an Express route handler
  • Calling next() more than once in the same middleware function
  • A catch block calling both res.send() and next(err)
  • An async callback (setTimeout, Promise, DB query) resolving after the response was already sent
  • Calling res.setHeader() after res.end() in raw Node.js http code
  • Fastify async handler both returning a value and calling reply.send()
How do I fix ERR_HTTP_HEADERS_SENT in Express?

Add return before every response call in your route handlers: return res.json({ error: 'not found' }). This ensures function execution stops when a response is sent. Also add return after every next() call in middleware, and fix any catch blocks that call both res.send() and next(err).

How do I use res.headersSent to prevent ERR_HTTP_HEADERS_SENT?

res.headersSent is true once any response data has been sent. Use it as a guard in async callbacks and error handlers:

setTimeout(() => {
  if (res.headersSent) return; // already responded
  res.status(503).json({ error: 'Timeout' });
}, 5000);

// In error middleware:
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  res.status(500).json({ error: err.message });
});
Why does ERR_HTTP_HEADERS_SENT happen in production but not locally?

Race conditions caused by timing-sensitive async operations usually only appear under load. Locally, requests are processed sequentially so a slow database query always resolves before a timeout fires. In production, under concurrent load, a slow async operation from one request path can complete after another code path has already sent a timeout or error response. The fix is to always guard async callbacks with if (res.headersSent) return; and use clearTimeout() to cancel timers when a result arrives.

How do I fix ERR_HTTP_HEADERS_SENT caused by double next() calls?

Add return after every next() call in your middleware:

// Before (broken):
function auth(req, res, next) {
  if (!req.user) {
    next(new Error('Unauthorized'));
    // code continues here!
  }
  next(); // called a second time for unauthorized requests
}

// After (fixed):
function auth(req, res, next) {
  if (!req.user) {
    return next(new Error('Unauthorized'));
  }
  return next();
}
How do I fix ERR_HTTP_HEADERS_SENT in Fastify?

In Fastify, async route handlers automatically send the return value as the response. If you also call reply.send() in the same handler, Fastify tries to send twice. Fix: either return the value (and never call reply.send()), or call reply.send() and return nothing. Check reply.sent before sending in defensive code.

How do I fix ERR_HTTP_HEADERS_SENT in Next.js API routes?

In Next.js API routes, the same rules apply as Express. Add return before every res.json(), res.send(), and res.redirect() call:

// pages/api/user.js
export default async function handler(req, res) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method Not Allowed' });
  }
  const user = await getUser(req.query.id);
  if (!user) {
    return res.status(404).json({ error: 'Not found' });
  }
  return res.json(user);
}

Related Errors