Step 1 — Find the Real Error
The browser shows "500 Internal Server Error" but that tells you nothing. The actual error is in your server logs.
| Platform | Log location |
|---|---|
| Node.js / Express | stdout/stderr, or your logging service |
| Django | DEBUG=True shows it in browser (dev only); production logs |
| nginx | /var/log/nginx/error.log |
| Apache | /var/log/apache2/error.log |
| Docker | docker logs <container> |
| AWS Lambda | CloudWatch Logs |
| Vercel / Netlify | Function logs in dashboard |
| Heroku | heroku logs --tail |
# Tail logs in real time while reproducing the error
tail -f /var/log/nginx/error.log
# or
docker logs -f my-appStep 2 — Common Causes
Unhandled exception / missing try-catch
The most common cause. An exception is thrown and nothing catches it.
// ❌ No error handling
app.get('/api/data', async (req, res) => {
const data = await db.query('SELECT * FROM users') // throws if DB is down
res.json(data)
})
// ✅ Catch and handle
app.get('/api/data', async (req, res) => {
try {
const data = await db.query('SELECT * FROM users')
res.json(data)
} catch (err) {
console.error('DB query failed:', err)
res.status(500).json({ error: 'Internal server error' })
}
})Database connection failure
Look for errors like ECONNREFUSED or Too many connections. Check that the database is running and the connection string is correct in environment variables.
Missing environment variable
A missing env var causes undefined errors at runtime. Validate all required env vars at startup.
# Check what env vars are set
printenv | grep DATABASE
# or in Node.js
console.log(process.env.DATABASE_URL)Syntax error in recently deployed code
A syntax error in a module causes the entire server to fail on startup or on first import.
# Node.js — check for syntax errors
node --check src/app.js
# Python
python -m py_compile app.pyOut of memory / resource exhaustion
Look for JavaScript heap out of memory or similar. Increase memory limits or find the memory leak.
File not found / wrong path
Paths that work locally may differ in production (especially in Docker). Use path.join(__dirname, 'config.json') instead of relative paths.
Step 3 — Add Proper Error Handling
Express.js — global error handler
// Must be defined AFTER all routes
app.use((err, req, res, next) => {
console.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
})
const status = err.status ?? 500
res.status(status).json({
error: status < 500 ? err.message : 'Internal server error'
})
})
// Use express-async-errors to catch async throws automatically
require('express-async-errors')Next.js App Router
// app/api/data/route.ts
export async function GET() {
try {
const data = await fetchData()
return Response.json(data)
} catch (err) {
console.error('GET /api/data:', err)
return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}Django
# settings.py — production settings
DEBUG = False
LOGGING = {
'version': 1,
'handlers': {
'file': {
'class': 'logging.FileHandler',
'filename': '/var/log/django/error.log',
},
},
'root': {
'handlers': ['file'],
'level': 'ERROR',
},
}Step 4 — Prevention Checklist
- All async operations wrapped in try/catch
- Global error handler catches unhandled exceptions
- Environment variables validated at startup (fail fast)
- Database connection errors handled gracefully
- Stack traces never sent to clients in production
- Health check endpoint (
/health) to verify DB/service connectivity - Structured logging with request context (path, method, user ID)
- Alerts configured for 5xx error rate spikes