Short answer
Find the second response in the stack trace and make every response the last thing that path does: write return res.status(400).json(...) instead of res.status(400).json(...) followed by more code. Don’t call next() after sending a response. In callbacks, loops and try/catch blocks, check that only one branch can send. In a custom error handler, check res.headersSent and pass the error to next(err) if a response has already started.
Why a second response is sent, and the fix
01
Missing return after sending
res.json(), res.send() and res.redirect() send the response but do not stop the function. Code after them runs and sends again. Return the call.
app.post('/signup', async (req, res) => { if (!req.body.email) { return res.status(400).json({ error: 'Email is required' }); } const user = await createUser(req.body); res.status(201).json(user); });02
next() called after a response
Middleware that sends a response and then calls next() lets the next route send its own. Middleware should do one or the other.
// bug: res.status(401).end(); next(); function requireUser(req, res, next) { if (!req.user) return res.status(401).end(); next(); }03
A response inside a loop, callback or both try and catch
res.send() inside forEach, or in a callback that can fire twice, or in try with another one after the catch, sends more than once. Collect the result first, then send one response at the end.
app.get('/report', async (req, res, next) => { try { const rows = await db.query(sql); return res.json(rows); } catch (err) { return next(err); } });04
An error handler that responds after streaming began
“you must delegate to the default Express error handler, when the headers have already been sent to the client” Express, Error handling: the default error handler
If an error happens while a response is already streaming, a custom error handler cannot send a new status. Check res.headersSent and hand the error to Express’s default handler.
app.use((err, req, res, next) => { if (res.headersSent) return next(err); res.status(500).json({ error: 'Something went wrong' }); });
Checked against the vendor’s docs on : Express: Error handling.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: Express “Cannot GET /” · “Uncaught (in promise)” errors · Node.js ECONNRESET (“socket hang up”)