HTTP 500 Internal Server Error — finding the real cause
Diagnose HTTP 500 by correlating a request id with server logs, exposing stack traces safely, and ruling out downstream dependency failures.
Symptoms
- HTTP 500 with an empty or generic body
- Only some requests fail while others succeed
- The error appears after a deploy or config change
Possible causes
- An unhandled exception in the request handler
- A downstream dependency (database, cache, service) is failing
- A configuration or environment value is missing in this environment
- A response was already partially sent when the error occurred
Diagnosis steps
Correlate a request id with the server log entry
Interpretation
Returning and logging a request id lets you find the exact stack trace for a user-reported 500.
Check downstream dependency health
Interpretation
Many 500s are really wrapped connection or timeout errors from a database or another service.
Diff configuration between working and failing environments
Interpretation
A missing env var or credential often appears only in one deployment.
Root cause
A 500 signals an unexpected condition the server did not handle. The generic body is deliberate, hiding internals, so diagnosis depends on server-side logs that carry the request id and stack trace. Frequently the true failure is downstream: a database timeout, a cache miss cascade, or a missing configuration value.
Fix
Ensure a top-level error handler logs the stack with a request id and returns a generic message to clients. Fix the underlying exception, add timeouts and circuit breakers around dependencies so their failures are attributed clearly, and validate required configuration at startup so missing values fail fast rather than per request.
Verify the fix
Replay the failing request with the same request id; it returns the expected success code, and logs show no new exception for that id.
Symptoms
Requests return 500 Internal Server Error with a generic body. Some requests succeed, so the process is alive; the failure is specific to a code path or dependency.
Diagnosis
Correlate the request id with logs and check dependencies:
app.use((req, res, next) => {
req.id = crypto.randomUUID();
next();
});
process.on('unhandledRejection', (err) => console.error(err));
Log the stack under the request id and return only a generic message.
Fix
Handle errors centrally and protect dependencies:
app.use((err, req, res, next) => {
console.error({ id: req.id, err: err.stack });
res.status(500).json({ error: 'internal', id: req.id });
});
Add timeouts to database and HTTP calls.
Verification
Replay the failing request; it succeeds, and the log for its request id contains no exception, confirming the underlying cause is resolved.
Additional Notes
The generic body is intentional, so recovery depends on correlating the client-visible identifier with a server-side record. Returning and logging the same request identifier is the single most useful habit, because it lets you find the exact stack for a user report without guessing at timing. Classifying the failure also matters: a downstream timeout wrapped as an internal error points at dependencies rather than handler logic, and separating those categories in logs shortens the investigation. Validating configuration at startup prevents a whole class of per-request errors that stem from a missing value.