Throwing Your Own Library Exceptions in PHP

Sarfraz Ahmed    April 27, 2015 06:06 PM

In the previous post, we saw the difference between errors and exceptions, how exceptions can be useful and created our custom exception handler. In this post, we will look how we can create custom exceptions specific to our application, library or company. We also talk about SPL Exceptions that should be used in your code where possible as best practice.

Background

Let's start with bit of background. If you worked on PHP's DOM, PDO, MySQLi or some other extensions or frameworks such as Symphony, Laravel, Slim or any other third party libraries, you might have noticed usually they throw their own exceptions:

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

$dbh = new PDO($dsn, $user, $password);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

In case of wrong database connection, it would throw PDOException:

PDOException: SQLSTATE[HY000] [1045] Access denied for user 'dbuser'@'localhost'

Where did PDOException come from ? Or how did PDO gave that exception ? It is simple. As I had pointed out in my previous post, Exception is a class like any other normal class that can be extended. That's exactly what PDO does here, it simply extends the Exception class so PDO extension should be doing something like this under the hood to be able to throw PDOException:

class PDOException extends Exception {}

And that's usually it. And then PDO should now be throwing that new exception instead of Exception:

// something went wrong
throw new PDOException('message here');

And then user of PDO sees those PDOExceptions.

Now that we know PDOException actually extends Exception, we can come to conclusion that we can catch exceptions thrown by PDO either by using PDOException or Exception through nested catch blocks. If exception is NOT caught for PDOException, it will fall back to base Exception:

try{
    // any PDO code here
}
catch(PDOException $e){
    // handle PDOException
}
catch(Exception $e){
    // handle Exception
}

Throwing Your Own Exceptions

As said above, we can throw our own exceptions by extending the Exception class:

class MyAwesomeLibraryException extends Exception {}

or

class MyCompanyException extends Exception {}

That's easy but one might ask why throw custom exceptions? I can see these reasons:

  • It makes it easy to recognize which thing (library, class, extension, etc) generated exception in code hierarchy
  • This helps developer of orginal library easily spot problems in their code
  • It brands your library exceptions like PDO, DOM, etc do.

Now as developer of some library/application, any time we find we need to throw exception, we simply throw our own custom exceptions:

// something wrong, throw our custom exception
throw new MyAwesomeLibraryException('some message');

Of course you can have many exception types as well for your application if you want.

Prioritizing Your Own Exceptions

Imagine we want to create our own ORM library called SuperORM and it uses PDO under the hood. We create our custom exception first:

class SuperORMException extends PDOException {}

And now we throw SuperORMException exception from whole of our ORM where needed. But since we are using PDO under the hood, we get its PDOException as well and we don't want to show this directly to the consumers of our SuperORM library, we want to be able to first show them our own exception type. This is how we can do that:

class SuperORMException extends PDOException {}

class SuperORM {
    public function connect($dsn, $user, $password) {
        // try connecting to database
        try {
            $dbh = new PDO($dsn, $user, $password);
            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        catch (PDOException $e) {
            throw new SuperORMException($e->getMessage(), null, $e);
        }        
    }
}

$dsn = 'mysql:dbname=testdb;host=127.0.0.1';
$user = 'dbuser';
$password = 'dbpass';

$superORM = new SuperORM;
$superORM->connect($dsn, $user, $password);

And that would result in our SuperORMException:

SuperORMException: SQLSTATE[HY000] [1045] Access denied for user 'dbuser'@'localhost'

And consumer of our SuperORM can catch our exception now:

try {
    $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
    $user = 'dbuser';
    $password = 'dbpass';

    $superORM = new SuperORM;
    $superORM->connect($dsn, $user, $password);
}
catch (SuperORMException $e) {
    echo $e->getMessage();
}

This makes sure consumer of library will now know that SuperORM will always throw exception of type SuperORMException.

We see that SuperORMException extends PDOException and PDOException extends Exception, this gives consumer the opportunity to catch exceptions in those types like so:

try {
    $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
    $user = 'dbuser';
    $password = 'dbpass';

    $superORM = new SuperORM;
    $superORM->connect($dsn, $user, $password);
}
catch (SuperORMException $e) {
    // code to catch exception
}
catch (PDOException $e) {
    // code to catch exception
}
catch (Exception $e) {
    // code to catch exception
}

So that's how you can throw your own custom exceptions. In fact that's how some of the ORMs or database libraries from various framework do and throw their own exceptions for consumers to catch.

Don't Always Throw Your Custom Exceptions

By this I mean there are certain exceptions types that are part of Standard PHP (SPL) known as SPL Exceptions. They are made to be thrown for specific reasons and considered best practice. You should throw SPL Exceptions where applicable instead of your own custom exceptions. Here they are:

As can be seen, those exceptions can be broadly divided into two main categories: LogicException and RuntimeException. All of those exceptions are self-explanatory from their names. For example LogicException represent any errors caused by the logical errors in your code, RuntimeException represent any errors that are caused after script has run eg runtime errors; similarly BadMethodCallException exception should be thrown if a method in your class doesn't exist and so on.

These SPL exceptions are excellent addition to PHP core because previously if call was made to some method which didn't exist, you usually communicated that to user through a message like:

throw new Exception('Method does not exist!');

So you communicated through those messages for different types of problems. The problem here was that we needed specific exceptions; if method didn't exist, we needed BadMethodCallException exception. This clearly tells developer what type of exception it is and why it might have come about. Another benefit is that such exceptions are useful even for Non-English speaking developers who can also easily spot the problem. Therefore, you must use SPL exceptions for different situations they are made for.







Comments powered by Disqus