Home / Articles / Web Development
Web Development

Error 500 Is Not a Diagnosis: How to Make PHP Errors Safer and More Useful

The message “Internal Server Error” is safe for users, but it hardly helps when the application needs fixing. With the right error handling pattern, PHP applications can provide a clearer experience without m...

Error 500 Bukan Diagnosis: Cara Membuat Error PHP Lebih Aman dan Berguna

The 500 Internal Server Error message often triggers panic: users don’t know what’s happening, while developers may not immediately know which part has failed. The issue is not that error 500 is wrong, but rather that it only describes the most common symptoms of a server-side failure.

A healthy web application does not need to be free of errors. What’s more realistic is to make errors easy to trace for the internal team while remaining safe and not confusing for users. The key lies in the separation between messages for users and information for developers.

Do not display technical details to everyone

When PHP encounters an exception or fatal error, information such as file names, SQL queries, folder paths, library versions, and even accidentally printed credentials can appear on the page. In development mode, these details are helpful. In production, the same details can provide clues for those looking to exploit vulnerabilities.

Therefore, applications should have two different behaviors:

  • Development: error details may be displayed or logged in full to facilitate quick debugging.
  • Production: users only receive a general message, while details are stored in a protected log.

Settings like display_errors need to be reviewed before the application is published. Hiding errors from the screen does not mean ignoring them. Errors must still be logged through a logging mechanism that can be checked by the team.

Use a single entry point for handling exceptions

Errors often become difficult to handle when each controller or PHP file has its own way of displaying messages. Some use try-catch, some print errors directly, and others simply return a blank page.

It’s better to prepare a single layer for exception handling. In modern applications, this layer can be in middleware, the front controller, or the application bootstrap. Its task is to catch unhandled exceptions, log the details, and then send an appropriate response.

try {
    $result = $service->processOrder($request);
    return responseJson($result, 200);
} catch (Throwable $exception) {
    $errorId = bin2hex(random_bytes(8));

    error_log(sprintf(
        '[%s] %s in %s:%d',
        $errorId,
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine()
    ));

    return responseJson([
        'message' => 'An error occurred on the server.',
        'error_id' => $errorId
    ], 500);
}

The example uses Throwable to handle both exceptions and some errors that can be caught. The actual implementation will need to adjust to the framework and application structure used.

Error IDs make user reports more useful

The phrase “The website is down” is too vague to act upon. In contrast, if the page displays a code like ERR-7F3A91C2, users can mention that code to the admin or customer service.

Error IDs do not need to contain information about the cause of the error. It’s actually better if the ID is random and not easily guessable. On the server side, the ID is associated with exception details, the time of occurrence, the endpoint, and other relevant technical information.

This way, the conversation between users and the support team becomes shorter:

“When saving the order, the code ERR-7F3A91C2 appeared at 14:05.”

The team does not need to guess which page is meant. They can search for that ID in the logs and see the corresponding event.

Differentiate between recoverable and non-recoverable errors

Not all issues should end as a 500 error. One common mistake is to assume every failure is a server problem, while the cause may stem from user input or business conditions that are indeed invalid.

  • 400 Bad Request: the request format is incorrect.
  • 401 Unauthorized: the user is not authenticated.
  • 403 Forbidden: the user is known but does not have permission.
  • 404 Not Found: the requested data or page was not found.
  • 409 Conflict: the request conflicts with the current data conditions.
  • 422 Unprocessable Content: the input is readable but does not meet validation rules.
  • 500 Internal Server Error: an unexpected failure on the server side.

This differentiation is important for the frontend, API clients, and support teams. If all responses use status 500, the application will struggle to distinguish between input that needs fixing and bugs that need immediate investigation.

Do not log arbitrary data

Logging is indeed important, but logs can also be a source of data leaks. Passwords, access tokens, card numbers, cookie contents, and personal data should not be logged in full.

Before writing something to the log, ask: is this data really needed to diagnose the problem? If the answer is no, do not log it. If it is needed, consider masking or partially obfuscating the value.

For example, emails can be displayed as na***@example.com, while tokens can be logged with only the last few characters—or not logged at all. Logs should also have access limits, retention periods, and clear deletion policies.

API responses must be consistent

For APIs, a consistent error format is far more useful than varying messages at each endpoint. Clients can check the same structure without writing specific logic for each type of failure.

{
  "error": {
    "code": "ORDER_PROCESSING_FAILED",
    "message": "The order cannot be processed yet.",
    "error_id": "ERR-7F3A91C2"
  }
}

The code field should be stable and usable by programs, while the message is intended for humans. Do not make the message text the sole basis for frontend logic because the text can change for language needs or user experience.

What can you do now?

  1. Check if error details are still displayed in production.
  2. Create a global mechanism to catch unhandled exceptions.
  3. Add error IDs to page and API responses.
  4. Use HTTP status codes according to the type of issue.
  5. Ensure logs do not store passwords, tokens, or sensitive data.
  6. Test error pages intentionally, not just waiting for bugs to appear.

A good error page does not need to look sophisticated. What matters is that users receive a calm explanation and a reasonable next step, while developers gain enough context to find the root of the problem.

With this approach, error 500 is no longer just a dead-end message. It becomes a meeting point between security, user experience, and a more orderly debugging process.

– Rio Yotto @rioyotto