Best Way to Handle Errors on a PHP Page?

As a developer, I’ve always admired the simplicity and clarity of Go’s error-handling pattern. In Go, functions often return a result alongside an error object, making it easy to handle errors explicitly. Recently, I started experimenting with applying this pattern in PHP, and the results have been surprisingly effective. I’ll walk you through how I implemented Go-style error handling in PHP, explain the code, and share some practical enhancements to make it more robust.

Go-Style Error Handling?

In Go, error handling is straightforward and explicit. Functions return both a result and an error, and it’s up to the caller to check for errors. This approach eliminates surprises and makes the code more predictable. PHP, on the other hand, traditionally relies on exceptions or return values for error handling, which can sometimes lead to less explicit and harder-to-maintain code.

By adopting Go’s pattern in PHP, we can achieve:

  • Explicit error handling: Errors are returned directly, making them impossible to ignore.
  • Consistency: A uniform way to handle errors across the codebase.
  • Clarity: Clear separation between successful results and errors.

Go-Style Error Handling in PHP

Let’s start with the basic implementation. The goal is to create a function that divides a number by 2 but handles errors like division by zero or invalid input.

PHP Code Breakdown

CustomError Class

class CustomError
{
    public function __construct(
        public string $message = '',
    ) {
    }
}
  • This class represents an error. It has a single property, message, to store the error description.
  • The constructor initializes the message property.

half Function

function half(int $a): float | CustomError {
    if ($a == 0) {
        return new CustomError("No zeros!\n");
    }
    return $a / 2;
}
  • This function takes an integer $a as input.
  • If $a is 0, it returns a CustomError object with the message "No zeros!\n".
  • Otherwise, it returns the result of dividing $a by 2.

Main Logic

$res = half(10);

if ($res instanceof CustomError) {
    echo $res->message;
    return;
}

echo $res . "\n";
  • The half function is called with the argument 10.
  • If the result is an instance of CustomError, the error message is printed, and the program exits.
  • If no error occurs, the result is printed.

Go Code for Comparison

For context, here’s how the same logic would look in Go:

package main

import (
	"errors"
	"fmt"
)

func half(a int) (float64, error) {
	if a == 0 {
		return 0, errors.New("No zeros!")
	}
	return float64(a) / 2, nil
}

func main() {
	res, err := half(10)

	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(res)
}
  • The half function returns a result and an error.
  • The caller checks if err is not nil and handles the error accordingly.

Adding More Practical Functionality

While the basic implementation works, it can be improved to handle real-world scenarios. Here are some enhancements:

Input Validation

We should ensure the input is a positive integer before performing the division.

function validateInput(int $a): ?CustomError {
    if ($a < 0) {
        return new CustomError("Input must be a positive integer.\n");
    }
    return null;
}
  • This function checks if the input is negative and returns a CustomError if it is.

Error Logging

Logging errors is crucial for debugging and monitoring.

function logError(CustomError $error): void {
    file_put_contents('error.log', $error->message, FILE_APPEND);
}
  • This function logs errors to a file (error.log).

Updated half Function

function half(int $a): float | CustomError {
    // Validate input
    $validationError = validateInput($a);
    if ($validationError !== null) {
        return $validationError;
    }

    // Handle division by zero
    if ($a == 0) {
        return new CustomError("No zeros allowed!\n");
    }

    return $a / 2;
}
  • The half function now validates the input and handles division by zero.

Main Logic with Logging

$res = half(10);

if ($res instanceof CustomError) {
    logError($res);
    echo $res->message;
    return;
}

echo "Result: " . $res . "\n";
  • If an error occurs, it’s logged and displayed.

Example Outputs

Valid Input

$res = half(10);
// Output: Result: 5

Division by Zero

$res = half(0);
// Output: No zeros allowed!
// Logs: "No zeros allowed!\n" to error.log

Invalid Input (Negative Number)

$res = half(-5);
// Output: Input must be a positive integer.
// Logs: "Input must be a positive integer.\n" to error.log

Benefits of This Approach

  1. Consistency: Errors are handled in a predictable and consistent manner.
  2. Separation of Concerns: Validation, error handling, and logging are separated into distinct functions.
  3. Scalability: The pattern can be extended to handle more complex error scenarios.
  4. Debugging: Logging errors makes it easier to debug issues in production.

Final Thoughts

Adopting Go-style error handling in PHP has been a game-changer for me. It brings clarity, robustness, and maintainability to the codebase. While PHP and Go are fundamentally different languages, borrowing best practices from one can significantly improve the other.

If you’re working on a PHP project and want to make your error handling more explicit and reliable, I highly recommend giving this approach a try. It might feel a bit unconventional at first, but the benefits are well worth it.

Related blog posts