HTTP 500 Internal Server Error — What It Means and How to Fix It
500 Internal Server Error
Verified against RFC 9110 §15.6.1 (HTTP Semantics), Node.js docs: process events, MDN Web Docs: 500 Internal Server Error · Updated June 2026
> quick_fix
Something crashed on the server. The client can do nothing — the fix is on the server side. Check the server's error logs immediately. The 500 response itself rarely contains useful debugging info (for security reasons), but the server logs will have the full stack trace.
# Check server logs immediately after a 500
# Node.js / PM2
pm2 logs --err --lines 50
# Docker
docker logs <container_id> --tail 50
# Nginx error log
tail -n 50 /var/log/nginx/error.log
# Railway / Vercel — check deployment logs in the dashboardWhat causes this error
HTTP 500 is a catch-all for any unhandled exception on the server. Common causes: unhandled promise rejection in Node.js, null pointer dereference, database query failure (connection lost, query timeout, constraint violation), missing environment variable, incorrect import path in production build, out-of-memory crash, or a syntax error introduced in a recent deployment.
How to fix it
- 01
step 1
Check the server logs immediately
500s are logged server-side with full stack traces. The response body is usually generic ('Internal Server Error') to avoid leaking implementation details. The stack trace in your log is the only path to the root cause.
# Kubernetes pod logs kubectl logs -f deployment/my-api --tail=100 # Systemd service journalctl -u my-service -f -n 100 # AWS CloudWatch aws logs tail /aws/lambda/my-function --follow - 02
step 2
Correlate the 500 with a deployment
Most 500s start after a deployment. Check your deployment history: what changed in the last release? Pay special attention to dependency version bumps, environment variable changes, and database migrations.
# Recent git changes git log --oneline -10 git diff HEAD~1 HEAD -- package.json # Environment variables diff — compare against working version # Never commit .env but do document changes in your deployment notes - 03
step 3
Reproduce locally with production-equivalent settings
Set `NODE_ENV=production` locally and run the exact build artifact. Many bugs only appear in production because dev mode has additional error handling, different module resolution, or disabled optimizations.
# Reproduce production build locally NODE_ENV=production npm run build NODE_ENV=production npm start # Test the exact failing request curl -X POST http://localhost:3000/api/failing-endpoint \ -H 'Content-Type: application/json' \ -d '{"same": "payload as production"}' - 04
step 4
Add global unhandled exception and rejection handlers
In Node.js, unhandled promise rejections cause 500s without appearing in request logs. Add global handlers to catch and log them with full context.
process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason) // In production: send to error tracker (Sentry, Datadog) }) process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err) process.exit(1) // Let the process manager restart the process }) - 05
step 5
Check for database connection failures
A lost database connection causes every subsequent query to throw, resulting in 500s on every request. Check connection pool health, `max_connections` limits, and whether your DB host is reachable.
# PostgreSQL — check active connections psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;" # Check max connections psql -U postgres -c "SHOW max_connections;" # Node.js — log pool state pool.on('error', (err) => console.error('Pool error:', err))
How to verify the fix
- Curl the failing endpoint — it now returns a 2xx response.
- Server logs show no new error entries during a test request.
- Error monitoring (Sentry, Datadog) shows 500 rate returning to baseline.
Why 500 happens at the runtime level
HTTP 500 is emitted by the application server when an unhandled exception propagates to the HTTP layer without being caught by a route handler. In Node.js/Express, this means an error thrown inside a middleware or route that wasn't passed to `next(err)`. In Python WSGI servers, it's an exception escaping the application callable. Web frameworks catch these at the framework level and serialize a 500 response rather than crashing the process. The key invariant: a 500 always has a corresponding exception in server-side logs — if there's no log entry, the 500 came from the proxy layer, not the app.
Common debug mistakes for 500
- Missing `try-catch` around async operations — especially `await` calls that can reject — causing unhandled promise rejections.
- Reading an environment variable without a fallback — `process.env.DATABASE_URL.split('/').pop()` throws if `DATABASE_URL` is undefined in production.
- Synchronous code in an Express route that throws — Express only catches errors passed to `next(err)`, not thrown exceptions in async callbacks.
- Database migration ran but application code still references the old schema — first request to that endpoint throws a column-not-found error.
- Production build has a different Node.js version than development — some native modules (better-sqlite3, bcrypt) must be rebuilt for the production Node version.
When 500 signals a deeper problem
A non-zero 500 rate in production means exceptions are reaching the HTTP layer — which means your error handling boundary is missing. Every route handler should have a try-catch or use an async wrapper that converts thrown exceptions into `next(err)` calls. Centralized error handling middleware should catch these, log them with request context (user ID, request ID, trace ID), send them to your error tracker, and return a structured 500 response. Without this, each 500 is a blind spot: you know something failed but have no context for why or for whom.
Editor's take
The 500 is the most expensive error code in production because it's opaque by design. You ship a release at 3pm on a Friday and at 3:15 your monitoring shows a spike in 500s. You have a 500 rate, a timestamp, and nothing else — no failing route, no user, no payload. The next 20 minutes determine whether you're an engineer who debugs production confidently or one who rolls back immediately and investigates offline.
The engineers who recover fastest have three things set up before a 500 ever appears: structured logging (every request logged with route, user ID, duration, and status), error tracking (Sentry or Datadog APM capturing exceptions with full stack trace and request context), and deployment markers in their observability platform (a vertical line showing when the bad deploy went out, so you can see 500 rate go from 0 to 5% the instant it deployed). Without all three, you're guessing.
The most common 500 I see in Node.js codebases: `Cannot read properties of undefined (reading 'id')` inside an async route handler where a database query returned `null` (resource not found) and the code assumed it would always return a row. The fix is one null check, but the root cause is missing integration tests that exercise the not-found case. The adjacent runtime error that produces the same 500 symptom in Next.js is `Error: ENOENT: no such file or directory` when a `fs.readFileSync` call references a path that exists on the developer's machine but not in the production Docker image — usually a data file that was in `.gitignore` or outside the Docker build context.
By Bikram Nath · Curator · Updated June 2026
Frequently asked questions
Why does the 500 response body just say 'Internal Server Error' with no details?
Intentional security design. Stack traces in HTTP responses leak file paths, library versions, and internal structure that attackers can use. Production servers return generic 500 messages. The full error is logged server-side. Configure your error tracker (Sentry, Rollbar) to capture exceptions with full context instead of surfacing them in the HTTP response.
How do I tell if the 500 is from my app or from the reverse proxy?
Check the response headers. A 500 from Nginx includes `Server: nginx`. A 500 from your Express app includes whatever you set in your `Server` header (or Express's default). Also check whether the response body contains an HTML Nginx error page vs your app's JSON error format. Proxy-level 500s appear in Nginx's error.log, not your app's logs.
Should I return 500 or let the process crash?
Catch the exception, log it with full context, and return a 500 response — then let the process manager (PM2, systemd, Kubernetes) restart the process if needed. An uncaught exception that crashes the Node.js process hard-kills all in-flight requests. A caught exception returns a 500 for the failing request and keeps the server alive for others. The exception: if you detect data corruption or a truly inconsistent state, crash intentionally rather than serve wrong data.