How to Fix a 500 Internal Server Error
A 500 is the server's way of saying "something broke and I couldn't finish your request" — without telling you what. Here is how to narrow it down from the browser, rule out the client, and find the line in your server logs that actually failed.
A 500 Internal Server Error is the most frustrating kind of HTTP error precisely because it tells you almost nothing. A 404 means the thing wasn't found; a 403 means you weren't allowed. But a 500 just means the server tried to handle your request and something went wrong on its side. It is a catch-all: the server is admitting failure without naming the failure. This guide is honest about that — the real cause of a 500 almost always lives in the server's logs, and the fix is not a trick you apply in the browser. What you can do from the client is narrow the problem down quickly, rule out your own code, and arrive at the log line that matters instead of guessing.
What a 500 actually means
A 500 Internal Server Error is a server-side status code indicating the server encountered an unexpected condition that prevented it from fulfilling the request. The request was received and understood, but the server failed while processing it. It is deliberately generic — the specifics of the failure are recorded in the server's own logs, not communicated in the status code.
The key word is server. A 500 tells you the request successfully travelled from the browser, across the network, and into your application — and then the application broke. That is genuinely useful information: it means the problem is not a typo in your URL, not a CORS block, not a network failure. The request arrived. Something in the code that ran to handle it threw, timed out, or hit a state it could not recover from. Your job is to find out which.
The common causes of a 500
A 500 is a symptom with many possible diseases. In rough order of how often they show up:
- An unhandled exception in server code. The single most common cause: a null reference, a failed type coercion, an array index that doesn't exist, an
awaitthat rejected without acatch. The code threw, no handler caught it, and the framework converted the crash into a 500. - A bad database query or connection. A malformed query, a missing table or column after a migration, a connection pool exhausted under load, or credentials that expired. The request handler ran, but the data layer underneath it failed.
- A misconfiguration. A missing or wrong environment variable, an incorrect file permission, a secret that wasn't set in production, or a path that exists locally but not on the server. Configuration bugs love to hide until the exact code path runs.
- A crashed or unreachable dependency. An upstream API your handler calls, a cache, a queue, or a microservice that is down or returning garbage. Your app is up, but something it depends on isn't.
- Out of memory. A request that allocates too much — a huge query result, an unbounded loop, a memory leak that finally tips over — and the process is killed or the runtime throws.
Notice that every one of these is on the server side, and none of them is visible from the status code alone. That is why the diagnosis workflow below moves deliberately from the client toward the logs — each step rules out a layer until you're standing in front of the code that actually failed.
How to diagnose it, step by step
Step 1 — Read the failing request in the Network tab. Open your browser's DevTools, switch to the Network tab, and reproduce the action. The failing request shows up with a red 500 status. Click it. The single most valuable thing here is the Response tab: many frameworks, especially in development mode, return a stack trace, an error message, or a unique error ID in the response body. Check the response headers too — an X-Request-Id or similar correlation header is the string you'll grep your logs for in a moment.
Step 2 — Reproduce with curl to rule out the client. Before you blame your frontend, confirm the server returns a 500 to a plain, dumb HTTP client with no JavaScript, no framework, and no browser state. If curl gets the same 500, the bug is unambiguously server-side.
# -i includes the response headers and status line in the output
curl -i https://api.example.com/v1/orders/42
# HTTP/2 500
# content-type: application/json
# x-request-id: 7f3c9a1e-... <- grep this ID in your server logs
#
# {"error":"internal_server_error","id":"7f3c9a1e-..."}
# For a POST, send the same body your app sends so you hit the same path:
curl -i -X POST https://api.example.com/v1/orders \
-H 'content-type: application/json' \
-d '{"sku":"ABC-123","qty":2}'If curl returns a clean 2xx but the browser gets a 500, the difference is in what the browser sent — an auth header, a cookie, a different payload — and that difference is your clue. If both get a 500, move to the logs.
Step 3 — Read the server logs. This is where the answer lives. The status code is a summary; the log line is the diagnosis. Open your application logs and your framework's error output and search for the timestamp of your request or the error ID from step 1. You are looking for the stack trace: the file, the line, and the exception type that actually threw. This is the point where the 500 stops being generic and becomes a specific, fixable bug — a NullPointerException, a connection refused, an undefined column.
To make step 3 possible, your server code has to actually log the failure with enough context. A bare handler that lets exceptions bubble up gives you a 500 with no trail. Wrap the risky work, log the error with a correlation ID, and return that ID to the client so the browser and the log can be matched:
// Express-style handler: catch, log with context, return a traceable ID
app.get('/v1/orders/:id', async (req, res) => {
const requestId = req.headers['x-request-id'] ?? crypto.randomUUID();
try {
const order = await db.orders.findById(req.params.id);
if (!order) return res.status(404).json({ error: 'not_found' });
res.json(order);
} catch (err) {
// The line that turns a mystery 500 into a findable log entry:
console.error(`[500] request ${requestId} failed`, {
route: req.path,
params: req.params,
message: err.message,
stack: err.stack,
});
res.status(500).json({ error: 'internal_server_error', id: requestId });
}
});Step 4 — Check recent deploys and config changes. A 500 that appeared suddenly, on code that worked yesterday, almost always traces to a change: a deploy, a migration, a rotated secret, an updated dependency, a changed environment variable. Before you go deep debugging the code, check what changed around the time the errors started. The correlation is often the whole answer.
500 vs 502, 504, and the 4xx errors
It is easy to lump all server errors together, but the 5xx codes point at different layers, and telling them apart saves you from debugging the wrong thing.
- 500 — your application server ran and threw. The bug is in the code that handled the request.
- 502 Bad Gateway — a proxy or load balancer in front of your app got an invalid or empty response from the upstream server. The gateway is fine; the thing behind it isn't answering correctly. See our guide on how to fix a 502 Bad Gateway.
- 504 Gateway Timeout — the same gateway waited for the upstream and it never answered in time. Suspect a slow or hung upstream, not your handler's logic.
The mental model: 500 is "my app broke," while 502 and 504 are "the thing my gateway talks to broke or stalled." If a proxy like Nginx, a load balancer, or a CDN sits in front of your app, a 502/504 usually means the request never got a clean answer from the app behind it.
And don't confuse any 5xx with the 4xx client errors. A 4xx (400, 401, 403, 404, 422) means the request itself was wrong — bad syntax, missing auth, forbidden, not found — and the fix is on the client side. A 5xx means the request was acceptable but the server failed to fulfil it. If you're unsure which class you're looking at, our HTTP status codes explained reference maps out every class and what it points to.
Fixing it, and stopping the next one
Once the log line names the exception, the fix is usually specific and small: catch the error the code was letting bubble, add the missing environment variable, correct the query, add a timeout and retry to the flaky upstream, or roll back the deploy that introduced it. The general principle is that a 500 should never be a surprise you learn about from a user. Two habits prevent most of them from being mysteries:
- Catch and log at your boundaries. Every request handler should convert an unexpected throw into a logged error with a correlation ID and a clean 500 response — never a silent crash. That is the difference between a five-minute fix and an afternoon of guessing.
- Return an error ID to the client. When the response body carries an ID that also appears in the logs, anyone — support, QA, or the user's own bug report — can hand you the exact string that leads to the failure. From the browser, that ID lives in the network response, which is exactly the artifact you want captured.
Install the free BugMojo extension and capture the session — replay, console, and network — when the error happens. You get the real failing request and any error ID the server returned, so you jump straight to the right log line instead of trying to reproduce it.
Install the free extensionFrequently asked questions
Frequently asked questions
Sources
- 500 Internal Server Error — MDN reference: the server encountered an unexpected condition that prevented it from fulfilling the request — MDN Web Docs (2026)
- HTTP response status codes — MDN reference: the full list, including the 5xx server-error class — MDN Web Docs (2026)
- RFC 9110: HTTP Semantics — the specification defining 500 and the 5xx server-error class — IETF RFC 9110 (2022)
- Response — MDN reference: the Fetch API Response object, whose body and headers carry the server's error detail — MDN Web Docs (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

