1. Library
  2. Computer Networks
  3. Http and the Web
  4. Status Codes

Updated 9 hours ago

When you see a 500 Internal Server Error, you're looking at a wall. The server is telling you "something broke on my end"—and nothing else. It's the most frustrating error to receive because it contains no information about what actually went wrong.

That's by design. The 500 is a catch-all. When the server encounters something unexpected—an unhandled exception, a crashed database, a null where there shouldn't be one—and doesn't have a more specific error to return, it defaults to 500. The error you see is never the error that happened.

What the Server Is Really Saying

A 500 response means:

  • The server received your request
  • Your request looked valid
  • Something unexpected happened during processing
  • The server won't tell you what
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
    "error": "Internal Server Error",
    "message": "An unexpected error occurred"
}

This tells you almost nothing. That's intentional. In production, servers hide their failures because exposing them would be a security risk. The real error—the stack trace, the database timeout, the null pointer—lives in the logs, not in the response.

What Actually Breaks

Unhandled Exceptions

The most common cause. Code throws an error that nobody catches:

app.get('/api/users/:id', async function(request, response) {
    // If this throws, there's no catch—straight to 500
    const user = await database.findUser(request.params.id);
    response.json(user);
});

Database Failures

The database is down, the connection pool is exhausted, or the query is malformed:

// Connection refused, query timeout, syntax error—all become 500
const data = await database.query('SELECT * FROM users');

Configuration Mistakes

A missing environment variable, a misconfigured service URL:

const apiKey = process.env.API_KEY; // undefined
await externalService.fetch(apiKey); // fails mysteriously

Resource Exhaustion

The server ran out of memory, disk space, or database connections. These are the hardest to debug because they don't always leave clean stack traces.

The Null Reference

The classic:

const user = getUser(id);
response.json({ name: user.name.toUpperCase() }); // user is null

The Two Faces of Error Responses

A 500 should show different faces depending on who's looking.

In production (what users see):

{
    "error": "Internal Server Error",
    "message": "An unexpected error occurred",
    "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

In development (what you see):

{
    "error": "TypeError",
    "message": "Cannot read property 'name' of null",
    "stack": "TypeError: Cannot read property 'name' of null\n    at /app/routes/users.js:15:32",
    "requestId": "550e8400-e29b-41d4-a716-446655440000"
}

The request ID is crucial. It's the bridge between the useless error the user sees and the real error in your logs. When a user reports "I got an error," that ID lets you find exactly what happened.

Catching What Falls

The goal is simple: nothing should fall through to an unhandled 500.

// Wrap individual routes
app.get('/api/data', async function(request, response) {
    try {
        const data = await fetchData();
        response.json(data);
    }
    catch(error) {
        logger.error('Failed to fetch data', { error, requestId: request.id });
        response.status(500).json({
            error: 'Failed to fetch data',
            requestId: request.id
        });
    }
});

// Global handler catches everything else
app.use(function(error, request, response, next) {
    logger.error({
        requestId: request.id,
        url: request.url,
        method: request.method,
        error: error.message,
        stack: error.stack
    });

    response.status(500).json({
        error: 'Internal Server Error',
        requestId: request.id
    });
});

What Clients Should Do

A 500 might be transient—a momentary database hiccup, a brief resource spike. Retry with backoff:

async function fetchWithRetry(url, maxRetries = 3) {
    for(let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url);

        if(response.status >= 500 && attempt < maxRetries - 1) {
            // Wait longer each time: 1s, 2s, 4s
            await sleep(Math.pow(2, attempt) * 1000);
            continue;
        }

        return response;
    }
}

And show users something human:

if(response.status === 500) {
    const error = await response.json();
    showError(
        'Something went wrong on our end. Please try again.',
        `Reference: ${error.requestId}`
    );
}

Finding the Real Error

The 500 is a symptom. The disease is in your logs.

# Find the request
grep "550e8400-e29b-41d4-a716" /var/log/app.log

# Find recent errors
grep -A 20 "Error:" /var/log/app.log | tail -100

Look for patterns:

  • Time-based: Errors during backups, at peak traffic, at midnight when cron jobs run
  • Endpoint-specific: One route failing more than others
  • User-specific: Certain accounts or data triggering failures

Error tracking services (Sentry, Rollbar, Bugsnag) make this easier—they aggregate errors, show stack traces, track frequency, and alert on spikes.

The Other 5xx Errors

Use 500 only when nothing more specific applies:

  • 502 Bad Gateway: Your server asked another server for something and got garbage back
  • 503 Service Unavailable: The server is temporarily overloaded or down for maintenance
  • 504 Gateway Timeout: Your server asked another server and never got an answer

These give clients better information. A 503 with a Retry-After header tells them when to come back. A 502 tells them the problem is upstream, not here.

The Mistakes That Hurt

Exposing internals:

// Dangerous—reveals database schema
{"error": "SequelizeDatabaseError: column 'api_key' does not exist"}

Swallowing errors:

catch(error) {
    // Error vanishes into the void—good luck debugging
    response.status(500).json({ error: 'Error' });
}

Using 500 for client mistakes:

// Wrong: the client sent bad data, that's a 400
POST /api/users {"email": "not-an-email"}
HTTP/1.1 500 Internal Server Error

The 500 is for when you broke something. The 400s are for when they did.

Frequently Asked Questions About 500 Internal Server Errors

Was this page helpful?

😔
🤨
😃