500 Internal Server Error (tested)
A 500 Internal Server Error means the server code failed while handling your request. The response almost never says why. The stack trace goes to the server's log, so the fix starts there.
1. What a 500 means
The request reached the app. The app crashed or threw an error it did not handle. The framework then sent a generic 500. It is a server-side bug or a server-side config problem. Retrying the same request usually gives the same 500.
It is different from the gateway errors. A 502 means a proxy got a bad reply from the app. A 503 means the service is down or overloaded. A 504 means the proxy gave up waiting.
2. What each framework sent in our test
Each handler threw an error. We called it with curl -i.
| Setup | Status | Body | Stack trace went to |
|---|---|---|---|
| Express 5, NODE_ENV unset (sync throw and async rejected promise) | 500 | HTML page with the full stack trace and file paths | Response body and stderr |
| Express 5, NODE_ENV=production | 500 | HTML <pre>Internal Server Error</pre> (148 bytes) | stderr only |
| Express 5 with custom error middleware | 500 | Whatever you send, e.g. JSON | Wherever you log it |
Plain Node http server, throw in handler | none | Empty reply, curl exit 52 | stderr, then the process exited with code 1 |
| Flask, debug off | 500 | Generic "Internal Server Error" HTML | stderr (ERROR in app: Exception on /x [GET]) |
| Flask, debug on | 500 | Werkzeug debugger page with the traceback (13 KB) | Response body and stderr |
| FastAPI on uvicorn | 500 | Internal Server Error, text/plain, 21 bytes | stderr (ERROR: Exception in ASGI application) |
Two lessons. First, the log has the real error in every case. Second, Express without NODE_ENV=production and Flask with debug on leak the stack trace to anyone who calls the URL. Do not run them that way in public.
3. Express 5: async errors are caught now
In Express 5, a thrown error and a rejected promise in an async handler both reached the error handler. Both gave a 500. Here is a custom handler. It must have four arguments, and it goes after your routes.
app.get('/async', async (req, res) => {
throw new Error('boom async');
});
app.use((err, req, res, next) => {
console.error(err); // keep the stack in your logs
res.status(500).json({ error: 'Internal Server Error' });
});
With this handler, both routes returned {"error":"Internal Server Error"} as JSON. The default HTML page was gone.
4. Plain Node: no 500 at all
A bare http.createServer handler that throws does not send a 500. In our test the whole process crashed. curl printed nothing and exited with code 52 (empty reply). Behind a proxy, this shows up as a 502 or a connection reset. Wrap handler code in try/catch and send the 500 yourself.
5. Clients and retries
In Node.js 24, fetch did not throw on a 500. It resolved with status 500 and ok false. Check res.ok yourself.
curl --retry 3 does retry a 500. Our test server counted 4 requests in about 7 seconds (1, 2 and 4 second waits). The server sent no Retry-After header. A retry only helps if the error is temporary.
curl -s -o /dev/null -w '%{http_code}\n' --retry 3 https://example.com/api
6. PHP, Apache and WordPress (from the docs)
We did not test these. This is what their docs say.
- PHP: with
display_errors=Off, a fatal error can give a blank page with a 500 status. The message goes to the PHP error log (error_login php.ini). - Apache: a typo or an unknown directive in
.htaccessmakes Apache return 500 for that directory. The Apache error log names the bad line. - WordPress: a fatal PHP error shows "There has been a critical error on this website." Set
WP_DEBUGandWP_DEBUG_LOGto true to writewp-content/debug.log. - File permissions: a script or file the server user cannot read or run can cause a 500.
- Memory limits: hitting PHP's
memory_limitis a fatal error, so it can also end as a 500.
7. Where to find the log
# nginx (as a proxy it logs upstream problems here)
tail -n 50 /var/log/nginx/error.log
# Apache
tail -n 50 /var/log/apache2/error.log # Debian/Ubuntu
tail -n 50 /var/log/httpd/error_log # RHEL/Fedora
# systemd service
journalctl -u myapp -n 100 --no-pager
# Docker container
docker logs --tail 100 mycontainer
Reproduce the request, then read the newest lines. Look for the first traceback or "ERROR" line near the request time.
8. Fix checklist
- Reproduce with
curl -iand note the time. - Read the app log or stderr from that moment.
- Fix the code or config the trace points to.
- Add an error handler that logs the error and returns a short, safe 500 body.
- In production, set
NODE_ENV=productionand keep Flask debug off.
Frequently asked questions
Is a 500 error my fault or the server's?
The server's. The request reached the app and the app failed. As a visitor you can only retry later or report it. As the owner, read the server log.
Why does the 500 page not show the error?
On purpose. In our test, Express with NODE_ENV=production, Flask with debug off and FastAPI all sent a short generic body. The full stack trace went to stderr, which ends up in your log.
Does Express 5 catch errors in async handlers?
Yes. In our test with Express 5.2.1, a rejected promise in an async handler gave a 500 and reached the custom (err, req, res, next) handler, the same as a sync throw.
Why do I get an empty reply instead of a 500?
Something crashed before a response was sent. Our plain Node http server crashed on a thrown error, and curl exited with code 52 (empty reply). Behind a proxy that often shows as a 502.
Should I retry a 500?
Once or twice at most, with a delay. curl --retry 3 sent 4 requests in about 7 seconds in our test. Most 500s are bugs, so retries give the same result until the code is fixed.
Written with AI assistance. Measured on 2026-10-02 with Express 5.2.1 on Node.js 24.21.0, a plain Node http server, Flask (Werkzeug 3.1.9), FastAPI on uvicorn (Python 3.13.5) and curl 8.14.1. The PHP, Apache and WordPress notes come from their docs, not from our run. PlainHeaders is our extension; curl, Node.js and Python are free.