Safeguard
Security Guides•

Does Express show stack traces to users, and what does NODE_ENV change?

Express's default error handler sends the stack trace to the client unless NODE_ENV is production. How to set it, and why a custom handler is still better.

Safeguard Security Team
3 min read

Yes. Express's built in error handler writes the stack trace into the response unless the app runs with NODE_ENV set to production. In production mode it sends only the status message. Set the variable on the process, and also add your own error handler so the output never depends on one setting.

What the docs say

The Express error handling guide states that if you pass an error to next and do not handle it in a custom handler, the built in handler writes the error to the client with the stack trace, and that the stack trace is not included in the production environment. The response body is the HTML of the status code message in production, otherwise it is err.stack.

The performance guide adds that NODE_ENV production makes Express cache view templates, cache CSS generated from extensions, and produce less verbose error messages.

Why the stack trace is a problem

A stack trace shows file paths, framework and library names, and sometimes values from error messages such as SQL fragments or internal hostnames. None of that helps a user. All of it helps someone probing your service, and it hints at which dependency versions you run.

Where to set NODE_ENV

The Express performance guide advises against setting environment variables by hand on a production server. It recommends your init system instead. With systemd, that means an Environment directive in the unit file:

[Service]
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /srv/app/server.js

In containers, set it in the image or the orchestrator's environment configuration. Then verify in a running instance, since a missing value fails open to the development behavior.

Steps to stop leaking errors

  1. Set NODE_ENV to production in every deployed environment, including staging, so staging behaves like production.
  2. Add a custom error handler as the last middleware. Log the full error on the server, return a short generic message to the client.
  3. Preserve intended status codes, for example 413 from body parsing, instead of flattening everything to 500.
  4. If headers were already sent, delegate to the default handler, as the guide explains, so the connection closes properly.
  5. Add a test that triggers an error and asserts the body contains no file paths.
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  req.log?.error(err);
  res.status(err.status || 500).json({ error: 'Request failed' });
});

Express's own security guide also suggests custom 404 and error messages to reduce fingerprinting, and notes that disabling X-Powered-By will not stop a determined attacker. See Express Helmet and fingerprinting.

Try it yourself

Click through it at your own pace, or open it full screen.

Click through the scan results and look for findings about verbose errors and response headers.

Safeguard's DAST scanning can test a running web app for responses like this, and a free scan is available at gold.safeguard.sh/scan.

Quick check

Is NODE_ENV production enough on its own?

It removes the stack trace from the default handler, but a custom handler gives you control over the message, logging and status codes regardless of the variable.

Does NODE_ENV change anything outside Express?

Many libraries read it, but behavior differs per library. This post only covers what the Express docs state.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.