Access and configuration

Fail without leaking internals

Return useful public errors while keeping stack traces, queries, paths, tokens, and infrastructure details in protected diagnostics.

Errors help users recover. They also help attackers learn. A stack trace that reaches the browser names your database, your file paths, your internal hostnames, and the exact library versions you run. Every one of those is a clue. The rule is to split the two audiences: a short public message for the user, a full private record for you.

What a leak looks like

The database goes down. Your Express app, running without NODE_ENV=production, answers with something like this:

Error: connect ECONNREFUSED 10.0.3.14:5432
    at /srv/app/node_modules/pg/lib/connection.js:52:11
    query: SELECT * FROM notes WHERE owner_id = $1

The user learns nothing useful from that. The attacker learns your database host, its port, a filesystem path, the driver, and a table name. A safe failure would have helped the user exactly as much.

Log the detail, return a code

Catch failures in one central handler. Record everything on the server, and send the client a stable code plus a request ID that ties the two together:

app.use((err, req, res, next) => {
  const requestId = req.id
  console.error({ requestId, message: err.message, stack: err.stack }) // stays server-side
  res.status(500).json({ error: 'internal_error', requestId })
})

Now the same outage produces this response body:

{ "error": "internal_error", "requestId": "b7e3f1a9" }

The user quotes b7e3f1a9 to support. Support searches the logs for it and finds the full stack, the query, and the host. Nothing in the response says anything about your internals.

Turn off the development error page

Frameworks ship a friendly error page for development, with the stack trace rendered in HTML. Make sure it’s off in production. In Express, setting NODE_ENV=production already stops the default handler from sending stack traces to the client:

NODE_ENV=production node server.js

Run your server without that variable, hit a route that throws, and you’ll see the stack in the browser. Run it with the variable and you get a bare Internal Server Error. That five-second check catches a surprising number of deployments.

Don’t go silent either

Removing every detail has its own cost. A response that says only error gives support nothing to search for, and users end up sending screenshots. The request ID is what makes the safe version usable. Keep it.

One more habit: test the unexpected failures, not only your clean validation errors. Validation messages are written by hand and rarely leak. The ones that leak are the exceptions you never planned for, like the database timing out or a JSON.parse on a corrupt body.

Try this on your own project: in production mode, trigger a validation error, a storage failure, and a thrown exception you didn’t handle. Save each public response and the matching log line. The response must contain no stack, query, path, host, or secret, and the request ID must connect both sides.

Lesson completed