Implement a global error handler that catches all unhandled exceptions and returns a sanitized response:
• Client response: Generic message only — { "error": "An unexpected error occurred", "requestId": "abc-123" }. Include a request ID so the user can report it and you can find it in logs.
• Internal log: Full exception with stack trace, input parameters, user context, and timestamp. Stored in your logging system (not returned to the client).
• Environment flag: Debug mode must be disabled in production. Set via environment variable, not source code. Deployment pipeline should validate DEBUG=false before allowing production deployment.
• Stack traces or exception messages
• SQL queries or database error messages
• Internal file paths
• Framework or library names and versions
• Environment variable names or values
• Server hostnames or internal IPs
Including a request ID in generic error responses solves a real operational problem: users can report “I got error abc-123” and you can find the full error detail in logs. This gives you debugging capability without exposing internals. Log the error, return the ID — users get something actionable, attackers get nothing useful.
Ask your team what a typical error message looks like in production when something goes wrong. If it includes database table names, file paths, or stack traces, those details are a map for attackers.