WEB Advent 2009 / Exceptional PHP

Call me crazy, but I love exceptions. I love exceptions to the rules. I love exceptions to language syntax, both programming and natural. I especially love programming exceptions.

The incredible power of programming exceptions — the ability to direct program flow based on a particular set of unusual conditions — is the coolest thing in the world. So, when I was asked to contribute to PHP Advent, I decided to write about my favorite programming tool.

Exceptions are objects

Exceptions are designed for use with object-oriented programming and are in fact objects themselves! This makes them incredibly useful.

Exceptions are special because they can be thrown using the throw keyword. You can also do lots of other things with them, such as pass them to other objects, manipulate them, &c. For example:

<?php
 
// This requires PHP 5.3.
class myClass
{
    public function testFunc()
    {
        try {
            $e = new Exception('my message');
            throw $e;
        } catch (Exception $e) {
            $ex = new Exception('nested exception', NULL, $e);
            throw $ex;
        }
    }
}
 
?>

Exceptions should be unique

The benefit of exceptions being objects is that you can extend the base Exception class while retaining its functionality. Take a look:

<?php
 
class MyException extends Exception
{
 
}
 
?>

Just like that, we have a custom exception called MyException. This brings me to my second point — exceptions can and should be unique. Each layer of an app should have its own unique exceptions, and you can take this a step further by creating a base exception and extending it to make your exceptions unique.

Exceptions should be unique for two reasons. First, for a practical reason, it simply makes it easier to see where the exception was raised. If you have an exception called CustomJsonLibException, for example, you know that all exceptions with that name should have been raised in the JSON library. Second, exceptions should not be allowed to “bubble up“ to the surface — they should be caught, handled, and possibly nested and rethrown by the various layers, ensuring that the layer invoked by the end user is ultimately the one that raises any exceptions.

Nesting is cool

When preserving layer abstraction, it’s sometimes hard to prevent the previous exception from vanishing. The previous exception probably contains some important information, so how do we make sure that it’s preserved while honoring layer abstraction? By nesting.

PHP 5.3 makes this easy. Nesting is built in as an optional third argument. In fact, the first example demonstrates this. You can also use the Exception::getPrevious() method to retrieve the previous exception and access the data through the Exception API.

Exception error codes

How does a developer distinguish between a “database not available” and a “table doesn’t exist” exception raised by the database layer? The exception message. How does the code make the same distinction? The exception code.

An exception code is the optional second argument that can be passed to exceptions. The code itself is an integer, and adding one makes handling exceptions much easier. I recommend using class constants as the integer values, because this makes it easier to remember them.

<?php
 
class MyException extends Exception
{
    const DB_TABLE_UNAVAILABLE = 10;
    const DB_DB_UNAVAILABLE = 20;    
}
 
class myClass
{
    public function testFunc()
    {
        try {
            // Connect to the database and fail.
            throw new MyException('Unable to connect to the database.', MyException::DB_DB_UNAVAILABLE);
        } catch (MyException $e) {
            switch ($e->getCode()) {
                case MyException::DB_TABLE_UNAVAILABLE:
                    $this->generateTable();
                    break;
                case MyException::DB_DB_UNAVAILABLE:
                    throw new MyException('There was a problem connecting to the database.', MyException::DB_DB_UNAVAILABLE, $e);
                    break;
            }
        }
    }
}
 
?>

Order catches more to less specifically

The more you use exceptions, the more likely it is that multiple exceptions of different types are raised in the same app. Never to fear! PHP permits developers to catch more than one type of exception that might have been raised in a try block.

When catching multiple exceptions, developers should go from the most specific exception to the least specific exception. This rule becomes even more important when the most specific exception might extend another exception that could also be raised. For example:

<?php
 
class MyException extends Exception {}
 
class DBException extends MyException {}
 
class DuplicateKeyException extends DBException {}
 
class myClass
{
    public function testFunc()
    {
        try {
            // An exception is raised.
        } catch (DuplicateKeyException $e) {
            // Handle exception.
        } catch (DBException $e) {
            // Handle exception.
        } catch (MyException $e) {
            // Handle exception.
        } catch (Exception $e) {
            // Handle catch-all.
        }
    }
}
 
?>

PHP tests each catch block to see if it meets the requirements to handle this particular exception. Because an exception of type DuplicateKeyException satisfies the requirements to be handled by any of the blocks, we must go from the most specific to the least specific. Had we listed the Exception catch block first, all exceptions raised would qualify to be handled by that block, and none of the other blocks would ever be run.

Exceptions are for exceptional situations

The name might make this seem obvious, but this is sometimes one of the most difficult things to grasp about exceptions. They are meant for error conditions. Sometimes developers like to raise exceptions as program control devices; this isn’t an appropriate use of the exception. For example:

<?php
 
function figureOutFlow($value)
{
    switch ($value) {
        case 'A':
            throw new AException();
            break;
        case 'B':
            throw new BException();
            break;
    }
}
 
?>

This function makes a terrible mistake; an exception is raised no matter what. The function doesn’t return anything. Exceptions are being used incorrectly here.

The general rule of thumb is that if you cannot remove all the exceptions and have the app operate properly (under normal conditions), you’ve misused your exceptions.

Exceptions are meant to be used for situations where there is an exceptional and unusual scenario, such as inappropriate or malformed data, missing resources, unimplemented methods, or errors from another part of the app.

Summary

Exceptions are my favorite part of programming, because they are powerful, versatile, and easy to use. The ability to nest, catch, rethrow, and manipulate them as objects makes them useful, fun, and helpful. Give them a try if you haven’t already!

Other posts