Express — Cannot set headers after they are sent
Fix "headers already sent" by ensuring each request sends exactly one response and guarding asynchronous paths.
Symptoms
- Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
- The client gets a 200 but the server logs an error
- A response is sent twice in different code branches
- An async handler sends after awaiting past a timeout
Possible causes
- A handler sends a response and then another branch also calls send
- A timer or callback fires after the response already completed
- Middleware next is called after responding
- A try/catch sends an error response after success was sent
Diagnosis steps
Find all response calls in the handler
grep -n "res.send\|res.json\|res.end\|res.status" routes/Interpretation
Listing every response call reveals branches that can each run, doubling the response.
Add a sent guard
if (res.headersSent) return;Interpretation
Checking headersSent before responding prevents a second send from crashing the process.
Clear timers on response close
res.on('close', () => clearTimeout(timer))Interpretation
Timers that outlive the response need clearing, or they attempt to write to a finished response.
Root cause
The code attempted to send a second response for a request that had already sent one, which the HTTP layer rejects because headers and body are already committed.
Fix
Ensure each code path sends exactly one response, add a headersSent guard, and stop asynchronously started work when the response closes.
Verify the fix
Reproduce the request and confirm no ERR_HTTP_HEADERS_SENT appears, and confirm the client receives a single well-formed response.
Symptom walkthrough
Express logs ERR_HTTP_HEADERS_SENT while the client still receives what looks like a successful response. The cause is two response attempts for one request: a handler sends and an error path also sends, or a timer or pending callback fires after the response completed. Because headers are already written, the second attempt throws and may crash the process.
Fix in practice
Restructure the handler so each path sends exactly one response, and guard with if (res.headersSent) return before any potential second send. Cancel timers and asynchronous work when the response closes so nothing writes after completion. Return after sending rather than falling through.
Verification
Reproduce the request and confirm the server no longer logs the error and the client receives one response. A single clean response with no server error proves the paths no longer double-send.
Diagnostic commands
Check the behavior with commands like these:
grep -n "res.send\|res.json\|res.end\|res.status" routes/
if (res.headersSent) return;
res.on('close', () => clearTimeout(timer))