Skip to main content
Network Errors

How to Debug HTTP 500 Errors in Your Web App

What a 500 Internal Server Error actually means, how to find the real error behind it, and common server-side causes with fixes.

Updated

How to Debug HTTP 500 Errors in Your Web App

You open the Network tab and see this:

500 Internal Server Error

A 500 means the server crashed while handling the request. It’s the server’s way of saying “something went wrong, but I’m not going to tell you what.” The response body might contain a generic error page, a stack trace (if you’re lucky), or nothing at all.

The 500 status code is a catch-all. It tells you the failure is server-side, not client-side. Your request was probably fine. The server received it, tried to do something with it, and failed. The real question is always: what went wrong on the server?

Where to look first

1. Check server logs

The browser can only show you the 500 status code and whatever the server chose to include in the response body. The actual error, with a stack trace and the exact line that failed, is almost always in the server logs.

Where those logs live depends on your stack:

  • Node.js/Express: stdout/stderr in the terminal, or wherever your logger writes
  • Next.js: The terminal running next dev, or your hosting platform’s function logs
  • Python/Django/FastAPI: The terminal, or journalctl if running under systemd
  • Cloudflare Workers: wrangler tail for real-time logs, or the Workers dashboard
  • Vercel/Netlify: Function logs in the platform dashboard under the specific deployment

If you’re running locally, the error is in your terminal. If you’re debugging a deployed app, check your hosting platform’s log viewer first.

2. Look at the response body

Some frameworks include error details in the response body during development. Express in development mode returns a stack trace in the HTML response. Django with DEBUG=True shows a detailed error page.

In production, most frameworks intentionally hide error details (which is correct for security). But during local development, the response body often has everything you need.

3. Reproduce with curl or Postman

Strip the request down to its minimum. Send the same request from curl to confirm it’s not a browser-specific issue. Include the same headers and body your frontend sends.

curl -v -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "test"}'

The -v flag shows the full request and response headers, which helps if the error is related to authentication headers, content types, or cookies.

Common causes

4. Unhandled exceptions

The most common cause. A function throws an error, and nothing catches it. The framework’s default error handler turns it into a 500.

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id)
  res.json({ name: user.name }) // crashes if user is null
})

Fix: Handle the error case explicitly.

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id)

  if (!user) {
    return res.status(404).json({ error: 'User not found' })
  }

  res.json({ name: user.name })
})

For Express specifically, unhandled promise rejections in async route handlers don’t automatically trigger the error handler in Express 4. You need to either wrap them in try/catch or use a wrapper function:

function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.users.findById(req.params.id)
  res.json(user)
}))

Express 5 handles this automatically.

5. Database connection failures

The server starts, but the database is unreachable. Every request that touches the database returns a 500. This is especially common in serverless environments where the database connection isn’t persistent.

Fix: Check that connection strings are correct, the database is running, and connection limits aren’t exhausted. For serverless, use a connection pooler like PgBouncer or Prisma Accelerate.

// Check the connection on startup
try {
  await db.raw('SELECT 1')
  console.log('Database connected')
} catch (err) {
  console.error('Database connection failed:', err.message)
  process.exit(1)
}

6. Missing or wrong environment variables

The server reads process.env.DATABASE_URL or process.env.API_KEY, gets undefined, and crashes when it tries to use the value. This is the number one cause of “it works locally but 500s in production.”

Fix: Validate environment variables at startup, before any request handling.

const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET']

for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required env var: ${key}`)
    process.exit(1)
  }
}

This way, a deploy with missing env vars fails immediately and loudly instead of silently 500-ing on every request.

7. Middleware errors

An error in middleware runs before your route handler. The route itself might be fine, but a middleware that runs on every request (authentication, body parsing, logging) throws and takes the whole request down.

Fix: Test each middleware in isolation. If every route returns 500, the problem is almost certainly in middleware or startup code, not in the route handlers. Comment out middleware one at a time to isolate the failure.

Check body parsing middleware specifically. If a request sends malformed JSON and your body parser throws without a try/catch, every POST/PUT endpoint returns 500.

Prevention

Build error handling into the server from the start:

  • Global error handler. Every framework has one. In Express, it’s the (err, req, res, next) middleware. Register it and log every error that reaches it.
  • Structured logging. Use a logger that outputs JSON with timestamps, request IDs, and stack traces. When a 500 happens in production, you need to find the exact log line quickly.
  • Health check endpoint. A /health route that checks database connectivity and other dependencies. If the health check fails, you know the problem before users report it.
  • Error monitoring. Tools like Sentry capture unhandled exceptions with full context (request body, headers, stack trace, user info). A 500 in production should trigger an alert, not wait for a user to complain.

Hushbug surfaces HTTP 500 errors and other failed network requests automatically while you browse. Coming soon to the Chrome Web Store.