When a handler catches an error and sends the raw message or stack trace back in the response, it exposes internal detail that helps an attacker: absolute file paths, framework and library versions, database column names, even fragments of failing queries. The fix is to log the full error where only you can see it and return a generic message with a reference id to the client.
Why it's a problem
Detailed errors turn blind probing into targeted attack. A leaked stack trace reveals the exact stack and versions to match against known exploits, a database error reveals table and column names for injection, and a path reveals your deployment layout. None of it is exploitable on its own, but together it removes the guesswork an attacker would otherwise need.
The pattern
app.get("/api/report", async (req, res) => {
try {
const data = await buildReport(req.query);
res.json(data);
} catch (err) {
// leaks message + stack to the caller
res.status(500).json({ error: err.message, stack: err.stack });
}
});The fix
app.get("/api/report", async (req, res) => {
try {
const data = await buildReport(req.query);
res.json(data);
} catch (err) {
const ref = crypto.randomUUID();
console.error(ref, err); // full detail stays server-side
res.status(500).json({ error: "Something went wrong", ref });
}
});Why AI tools write this
Echoing err.message back to the client is the shortest way to make an error visible during development, and it is genuinely useful while building. The assistant writes the debugging-friendly version and there is no prompt telling it to switch to a production-safe response, so the leak ships as written.
The quick fix
- Log the full error and stack server-side; return a generic message to the client.
- Attach a random reference id to both so you can correlate a report to a log entry.
- Disable framework debug or verbose error pages in production.
- Never include err.stack, query text, or file paths in a response body.
Common questions
How do I find where my app leaks stack traces?
Search for err.stack, err.message, and error.stack in response-building code. Check your global error handler specifically, since that is where unhandled errors are serialized into responses.
Is returning a generic error message enough if the HTTP status code reveals something?
HTTP status codes are generally fine to expose: 404, 401, 403, 500 are expected. The risk is the message body, which can include file paths, query structure, and library versions.
How do I correlate a production error to a log entry without exposing details to the user?
Generate a random reference id with crypto.randomUUID() at the catch boundary. Log the full error with that id server-side, and return only the id to the client for support correlation.