How to use error handling in PHP

Modularize your error handling.

There is nothing worse than having errors you don't see. There are mistakes we make and IDEs do their best to notify us as we type. Things like syntax errors, missing semi colons, wrong data type assignment. These things happen and the compiler throws an error before we launch our application. The worse kind of errors are those that only happen in specific conditions.

I wrote before about how errors should be embraced and used as a tool to improve your application. For the occasions where the application only result in error sometimes, I suggest you throw an error yourself.

When connecting to a database in php, it is common to see in tutorials the use of task() or die(). This is very useful, but only during development. Saying the database died and showing the error might be useful for the developer but not so much for the users. It might reveal application secrets you actually don't want to share. Instead, use this opportunity to gracefully degrade the experience. Here is an example.

$con = new mysqli($host, $user,$pass,$dbname);
$sql = "SELECT * FROM articles WHERE article_id=" .$sanitized_id. " LIMIT 1";
$result = $con->query($sql);
$article = $result->fetch_array();

This code will work fine if the query is correct, if the user input is correct, and if everything is fine with the database. However, if any part fails you won't be aware of it. Since we know which part is likely to fail, we can add error handlers at each point of failure.

$con = new mysqli($host, $user,$pass,$dbname);
if ($con->connect_errorno){
    // handle error
}
$sql = "SELECT * FROM articles WHERE article_id=" .$sanitized_id. " LIMIT 1";
$result = $con->query($sql);
if (!$result){
    // handle error
}

$article = $result->fetch_array();

Now in case the code fails, we have two places where we can handle the errors. But, how should we handle those errors? Simply knowing is not enough. We can print the error on the page and exit but this will be useless to users.

$con = new mysqli($host, $user,$pass,$dbname);
if ($con->connect_errorno){
    echo "Error: Mysql Failed to connect. \nError #{$con->connect_errorno}:". $con->connect_error;
    exit;
}
$sql = "SELECT * FROM articles WHERE article_id=" .$sanitized_id. " LIMIT 1";
$result = $con->query($sql);
if (!$result){
    echo "Error in the query. \n\n".$con->error}";
    exit;
}

During development this should work fine for us. Our application halts entirely until we fix the problem. But it won't be ideal in a live server because only the user sees these errors.

The number one thing I do when working on my dev machine, is enable every error. There is no place for @ error silencer in my code. Not only there are performance benefits for doing so, it also allows you to find problems much faster.

if (APP_ENVIRONMENT === 'dev'){
    ini_set("display_errors","On");
    error_reporting(E_ALL);
}

I put this very high up in my code to make sure I don't miss any error.

We can handle errors using an error class. To make it simpler, we are not going to extend the Exception class for this particular example. Instead we will manually throw errors where they need to be. We will write how we want our code to work first before the actual implementation.

$con = new mysqli($host, $user,$pass,$dbname);
if ($con->connect_errorno){
    Error::throwDBError($con->connect_error,$con->connect_errorno,"Failed to Connect");
}
$sql = "SELECT * FROM articles WHERE article_id=" .$sanitized_id. " LIMIT 1";
$result = $con->query($sql);
if (!$result){
    Error::throwDBError($con->error,null,"Query:\n$sql");
}

We can use our error class to throw the appropriate errors and when we implement, we can do stuff like displaying a generic error page if it is a user and log the error for the ourselves. One thing that other languages have for free is a stack trace. When you get an error, you want to see the steps your application took to arrive at the error. This will help a lot when debugging your program.

class Error {

    private static $errorPagePath = "/path/to/errorpage.php";

    public static function throwDBError($error,$errornumber,$message){
        $error = new Exception();
        $trace = $e->getTraceAsString();
        self::clearContent();
        $content = "$error $errornumber:\n\n$message";

        header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);

        if (APP_ENVIRONMENT === 'dev'){
            header("Content-Type: text/plain");
            echo $content;
            exit;
        }
        // for users show error page
        error_log($content);
        include(self::$errorPagePath);
        exit;
    }
}

When we call the throwDBError() function, the correct error is displayed to user, and we get a log. We can easily upgrade it to send an email if it's the first time this error occurred if we want to.

This class can be reorganized to add multiple types of errors to cater to our application. The goal here is to have control over errors. When we limit all errors to known point of failures we have the luxury of knowing exactly what they are and how to fix them.

Here is a class that can be extended to handle multiple type of errors.

class Error {
    private static $errorPagePath = "/path/to/errorpage.php";

    public static function throw404($message);

    public static function throw500($message);

    public static function throwDBerror($error,$errornumber,$message);

    ...
}

Remember that detailed errors are for the developers only not your users.


Comments

There are no comments added yet.

Let's hear your thoughts

For my eyes only