TailTemplate Build stunning websites faster with our pre-designed Tailwind CSS templates

Modern PHP Developer - Exception

Since PHP 5 was released, Exception is added to PHP as an object-oriented programming language feature. By definition, an Exception is an exceptional event during program execution. In PHP, an Exception is simply an object (an instance of Exception class). When an exception occurs, PHP will halt current execution flow and look for an handler, and then it will continue its execution by the handler's code. If no handler is found, a PHP Fatal Error will be issued with an "Uncaught Exception ..." message and the program terminates.

When to use Exception

Exception is good for handling exceptional cases of your program, however it is not the solution for all error cases. Sometimes it is perfectly fine to return a boolean FALSE. Sometimes you are much better off throwing exceptions instead of returning weird error codes. Therefore it is very important to understand when to use Exception and when not to.

By now, we all know an exception should be thrown when an exceptional situations occurs. But if exceptional situation seems rather arbitrary, what qualifies as an "exceptional" situation?

Here is a good rule of thumb: since exceptional situations don't happen frequently, if you supply correct values to your function and remove the thrown exception, if the function then fails, the exception is used incorrectly.

Let's take a look at some concrete examples:

  • If you create a function to save a user's input to database, when a database connection fails, an exception should be thrown.
  • For the same function, you create a validator for checking a user's input. When an invalid value is supplied, you should not throw an exception. Invalid value is a rather frequent case for a validator class.

A good use case of Exception

Here we have an example of returning error codes to indicate error cases:

    ...
    public function login()
    {
        if ($this->invalidUsernameOrPassword()) {
            return -2;
        }
        if ($this->tooManyLoginAttempts()) {
            return -1;
        }
        return 1;
    }
 
    public function redirectToUserPage()
    {
        ...
    }
}

Client code might be something similar to the below:

$user = new User();
$result = $user->login();
if (-2 == $result) {
    log('invalid username or password');
} else if (-1 == $result) {
    log('too many login attempts');
} else if (1 == $result) {
    $user->redirectToUserPage();
}

Here we can spot a couple of problems with error codes:

  • Error codes do not contain error related information by themselves, which make them very hard to maintain.
  • Error codes result in number of if/else statements in client's code, depending on how many there are. (Conditional statements should be eliminated as much as possible, in order to make our code clean).

Let's refactor the code to use exceptions:

class User {
    ...
 
    public function login()
    {
        if ($this->invalidUsernameOrPassword()) {
            throw new InvalidCredentialException('Invalid username or password');
        }
 
        if ($this->tooManyLoginAttempts()) {
            throw new LoginAttemptsException('Too many login attempts');
        }
    }
 
    public function redirectToUserPage()
    {
        ...
    }
}

And client code can be refactored as:

try {
    $user = new User();
    $user->login();
    $user->redirectToUserPage();
} catch (InvalidCredentialException $e) {
    log($e->getMessage());
} catch (LoginAttemptsException $e) {
    log($e->getMessage());
}

As we can see, by using exceptions, the second code sample conveys messages about the errors much more clearly. Beyond that, in the client code, by eliminating conditional statements, the code become self-explanatory.

A case of misusing Exception

One common way of misusing exceptions is using them to control the flow of application logic. It is not only confusing, but also slows down your code. To emphasize again, exception is used to raise exceptional situations.

Below is one example of of misusing exceptions, which is discouraged.

As we can see, by using exceptions, the second code sample conveys messages about the errors much more clearly. Beyond that, in the client code, by eliminating conditional statements, the code become self-explanatory.

A case of misusing Exception

One common way of misusing exceptions is using them to control the flow of application logic. It is not only confusing, but also slows down your code. To emphasize again, exception is used to raise exceptional situations.

Below is one example of of misusing exceptions, which is discouraged.

The function register() uses exceptions to delegate account creation tasks. This is obviously against rules of exceptions. Though PHP does not stop you, you should religiously forbid yourself from doing this.

How to use Exception

Four key words are associated with Exception. They are: throw , try , catch and finally . An exception object is thrown( throw ) in a method when an exceptional event happens. Clients that call the method will normally place the method in a try block and catch it with some handling code. A finally block ensures the code inside the block will always be executed.

Throw

All exceptions in PHP are a class or subclass of Exception. It takes three optional parameters in its constructor.

public __construct ([ string $message = "" [, int $code = 0 [, Exception $previous = NULL)
  • $message: The exception message. This message provides some human readable information. And normally supply this parameter when instantiating an exception.
  • $code: It is useful for identifying types of exceptions that belong to the same class.
  • $previous: The exception before current one. This is normally used when we want to re- throw an exception in a catch block.

Below is an example of PHP syntax to throw an exception

throw new Exception('some error message');

The keyword here is throw . Note that we first need to initiate an exception object.

Catch

When we need to catch exceptions, we place the callee in a try-catch block as below:

try {
    methodThatThrowsExceptions();
} catch (Exception $e) {
    // handle exception gracefully
}

The catch block is where we place our handler code. Detailed exception handling implementation varies depending on application design. For example, we could try to recover the exception as much as we can, and if that is not possible, we could redirect users to a customer support page. If we leave it unhanded, PHP will eventually terminate the program and leave users with a page of meaningless error message, which is discouraged.

Exception bubble effect

If you are using some kind of frameworks, exceptions are likely handled even if you never actually create any handler for them. That is because exceptions bubble up, and your framework will eventually handle them. A simple example of exception bubble effect is:

function methodA()
{
    throw new Exception('error from methodA');
}
 
function methodB()
{
    methodA();
}
 
function methodC()
{
    try {
        methodB();
    } catch (Exception $e) {
        // handle error gracefully
    }
}

In the sample code, when methodC is called, it calls methodB, which directly invokes methodA. An exception is thrown in methodA, since methodB does not handle it. It then bubbles up to methodC, which gracefully handles the exception. In this example, though methodC does not call methodA directly, because exception bubbles up the stack, it is still handled gracefully at the end.

Multiple catch blocks

A method might contain different exceptions: some might be directly thrown by themselves and some might be bubbled up from their underlying stack. The catch block is designed to handle multiple exceptions, so we can have multiple catch blocks for handling different exceptions. A caveat here is that the position matters.

In multiple catch blocks, PHP picks the first block that matches the thrown exception's type. A good rule for positioning catch blocks is from a more specific one to a less specific one.

Let's see it in an example.

class ExceptionA extends Exception{}
 
class ExceptionB extends ExceptionA{}
 
try {
    methodThatThrowsExceptionA();
 
} catch (ExceptionA $e) {
 
} catch (ExceptionB $e) {
 
} catch (Exception $e) {
 
}

In the sample code, it is obvious ExceptionA catch block will get picked. Now let's change the method to throw ExceptionB.

class ExceptionA extends Exception{}
 
class ExceptionB extends ExceptionA{}
 
try {
 
    methodThatThrowsExceptionB();
 
} catch (ExceptionA $e) {
 
} catch (ExceptionB $e) {
 
} catch (Exception $e) {
 
}

Which catch block do you think it will be picked? The answer is still ExceptionA. Because ExceptionA is a parent class of ExceptionB, when ExceptionB is thrown, ExceptionA catch block comes first and matches the thrown exception's type, giving ExceptionB is an instance of ExceptionA.

This can be fixed by positioning them from the the more specific type to a less specific type, as shown below:

class ExceptionA extends Exception{}
 
class ExceptionB extends ExceptionA{}
 
try {
 
    methodThatThrowsExceptionB();
 
} catch (ExceptionB $e) {
 
} catch (ExceptionA $e) {
 
} catch (Exception $e) {
 
}

trace message

Since exceptions can be thrown anywhere in your program, it is very important to find the root cause. Exception provides various APIs to make it easy to trace where the exception comes from.

Seven public methods are extracted from PHP manual file:

  • final public string getMessage ( void )
  • final public Exception getPrevious ( void )
  • final public mixed getCode ( void )
  • final public string getFile ( void )
  • final public int getLine ( void )
  • final public array getTrace ( void )
  • final public string getTraceAsString ( void )

And we can use them to trace details of the thrown exception:

  • Exception::getMessage — Gets the Exception message
  • Exception::getPrevious — Returns previous
  • Exception Exception::getCode — Gets the Exception code
  • Exception::getFile — Gets the file in which the exception occurred
  • Exception::getLine — Gets the line in which the exception occurred
  • Exception::getTrace — Gets the stack trace
  • Exception::getTraceAsString — Gets the stack trace as a string

Let's put it in action.

For demonstration purpose, let's pretend we have a createAccount() method, which throws an Exception when an email address is invalid.

function createAccount($email)
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new Exception('In valid email address');
    }
 
    return sprintf('account creation email is sent to %s', $email);
}

What information can we get if we trigger the exception?

function linSeparator()
{
    return PHP_EOL.'----------------------------------'.PHP_EOL;
}
 
try {
 
    createAccount('test');
 
} catch (Exception $e) {
    echo $e->getMessage();
    echo linSeparator();
 
    echo $e->getPrevious();
    echo linSeparator();
 
    echo $e->getCode();
    echo linSeparator();
 
    echo $e->getFile();
    echo linSeparator();
 
    print_r($e->getTrace());
    echo linSeparator();
    echo $e->getTraceAsString();
}

Running the script from CLI, we get the following:

In valid email address
----------------------------------
 
----------------------------------
0
----------------------------------
/Users/xu/Desktop/Exception/trace.php
 
----------------------------------
Array
(
    [0] => Array (
        [file] => /Users/xu/Desktop/Exception/trace.php
        [line] => 19
        [function] => createAccount
        [args] => Array
        (
            [0] => test
        )
    )
)
 
----------------------------------
#0 /Users/xu/Desktop/Exception/trace.php(19): createAccount('test')

Beside obvious trace information, we can also tell that, the default code is 0 and previous exception is null when instantiating an exception object.

?

public __construct (string $message = "" , int $code = 0 , Exception $previous = NULL);

One more point we want to address here is that an exception is created when it is instantiated, not when it is thrown. So Exception APIs will give you information related to the time an exception is instantiated.

For example, in the method below, Exception::getLine will return 2.

function methodThrowException()
{
    $exception = new Exception('error from methodThrowException'); // line 2
    throw $exception; // line 3
}

finally

Before PHP 5.5, there was not finally block in PHP. A problem surfaced. If we wanted to ensure that one piece of code always ran regardless of which catch block got picked, we had to put that piece of code into each one of catch blocks.

To solve this problem, finally block was introduced since PHP 5.5. Code inside finally block will always be executed after catch block. We can even use try/catch only without catch .

This block is a place for us to do some clean up jobs. Tasks such as rolling back database transactions, closing database connections, releasing file locks and so on. Its usage is pretty straightforward.

For example, to use it alongside try/catch blocks:

try {
    createAccount('test');
} catch (Exception $e) {
    echo $e->getMessage();
} finally {
    echo 'Close Database Connection';
}

Use try/finally block only:

try {
    createAccount('test');
} finally {
    echo 'Close Database Connection';
}

Create your first custom exception

Throwing custom exception allows client code to handle the error case in a recognized manner. For example, when a database exception is thrown, it is reasonable to shut down the process completed. However, in the case of an invalid user input, we might just want to log an error message.

By creating custom exceptions, we express error cases of our code proactively. This not only helps the clients to avoid pitfalls, but also gives them enough information to handle the error cases confidently.

As all exceptions in PHP 5.x use Exception as the base, we are actually extending Exception to create our custom exception. In this example, let's revisit our previous code.

class User {
    ...
 
    public function login()
    {
        if ($this->invalidUsernameOrPassword()) {
            throw new InvalidCredentialException('Invalid username or password');
        }
        if ($this->tooManyLoginAttempts()) {
            throw new LoginAttemptsException('Too many login attempts');
        }
    }
 
    public function redirectToUserPage()
    {
    ...
    }
}

We have two custom exceptions here (InvalidCredentialException and LoginAttemptsException). They should actually be under one type. And they will be assign different messages.

Since InvalidCredentialException and LoginAttemptsException are error cases for an invalid login runtime error, it is reasonable to create an exception called InvalidLoginException, and use it for the two error cases above.

It is simple enough to create a custom exception with only one line of code.

class InvalidLoginException extends Exception {}

We can refactor previous code to use the newly created exception class:

class User {
    ...
    public function login()
    {
        if ($this->invalidUsernameOrPassword()) {
            throw new InvalidLoginException('Invalid username or password');
        }
 
        if ($this->tooManyLoginAttempts()) {
            throw new InvalidLoginException('Too many login attempts');
        }
    }
 
    public function redirectToUserPage()
    {
    ...
    }
}

A little trick

A potential issue might appear shortly, if we are using InvalidLoginException with too many different messages. The issue is easy to illustrate.

Imagine somewhere in our code, we need to throw another InvalidLoginException when an user account is blocked. We will throw the exact InvalidLoginException, but with different messages. The same thing happens again, and we will repeat the same actions. The list adds up. Now imagine doing this for different types of exceptions. We would lose track as the developer.

So here is a little trick: we shift the exception creation task to InvalidLoginException class.

class InvalidLoginException extends Exception
{
    public static function invalidUsernameOrPassword() {
        return new static('Invalid username or password');
    }
 
    public static function tooManyLoginAttempts() {
        return new static('Too many login attempts');
    }
}

Now the client code becomes:

class User {
    ...
    public function login()
    {
        if ($this->invalidUsernameOrPassword()) {
            throw InvalidLoginException::invalidUsernameOrPassword();
        }
 
        if ($this->tooManyLoginAttempts()) {
            throw InvalidLoginException::tooManyLoginAttempts();
        }
    }
 
    public function redirectToUserPage()
    {
    ...
    }
}

When the instantiation of exception is shifted to a function block, we gain much more space and freedom to do more interesting stuff, compared to a single line within the if block previously.

By keeping all of them in a centralized location, which is the exception class itself, not only does it create a more maintainable code base, but also give clients an opportunity to take a quick glance what exact exception they would expect.

SPL exceptions

Creating your own custom exception is great, but it does take some mental energy to come out as a meaningful name. Naming is hard, arguably one of the hardest thing in programming.

The Standard PHP Library (SPL) provides a set of standard Exceptions. We should use them for our own benefit. They cover a list of common error cases, which can save us energy if we were to come out on our own. Additionally, we can also expand from these standard Exceptions to make them more specific to our own domain.

In this sections, we will go through 14 SPL Exceptions, explaining them in simplest terms, so that you can use them next time in your own project.

-LogicException (extends Exception)
    --BadFunctionCallException
    --BadMethodCallException
    --DomainException
    --InvalidArgumentException
    --LengthException
    --OutOfRangeException
-RuntimeException (extends Exception)
    --OutOfBoundsException
    --OverflowException
    --RangeException
    --UnderflowException
    --UnexpectedValueException

There are two main categories of SPL Exceptions. They are LogicException and RuntimeException, Under each one of them, there are a few sub exception classes describing more specific error cases.

Logic Errors

LogicExcetpion

It's not hard to tell from its name that, LogicException covers error cases related to logics. As it is a parent class for a few more specific exceptions, it is a bit generic. When your code is returning or receiving something that is not logic, there is a logic error. When an error case is determined to be a logic error, use LogicException if you cannot find a better fit from its subclasses.

BadFunctionCallException

When an non-existing function get called, or wrong parameters are supplied to a function, this exception gets thrown. As this exception covers function scope, not method in a class, it is mostly thrown by PHP.

BadMethodCallException

When an non-existing method of a class gets called, or wrong parameters are supplied to that method, an BadFunctionCallException can be thrown. While this exception is similar to to BadFunctionCallException, it is designed for class scope.

DomainException

Domain here refers to the business our code works for. When a parameter is valid by its datatype, but invalid to the domain, a DomainException can be thrown.

For example, in an universal image manipulation function, transformImage($imageType) , DomainException should be thrown when $imageType contains an invalid image type. To this domain, an invalid image type is a domain error.

InvalidArgumentException

This one is simple, as its name says: it should be thrown when an invalid argument is supplied.

PHP5 introduces type hinting, however it does not yet work for scalar types such as int, string yet. To make it work, we throw InvalidArgumentException when a scalar type does not meet the requirement.

LengthException

We can use this exception when length of something is invalid. For example, password needs to be at least 8 characters.

OutOfRangeException

Use this exception when an invalid index is accessed. The keyword here is range.

RuntimeException

RuntimeException is a name derived from compile language, such as Java. In Java, there are two main categories of exception: checked exceptions and runtime exceptions. A complier won't compile the code until all checked exception are handled (in a catch block). Runtime exception can only be detected at run time and is not required to place these exceptions in a catch block.

Since PHP is not a compile language, we can think of its "compile time" as the time we write the code, and its "run time" as the code execution time. "Compile time" exceptions can be detected in development time, for example invalid datatype parameter.

To avoid confusion, keep in mind that logic exceptions discussed above are for "compile time".

RuntimeException's subclasses cover more specific scenarios. Use this exception if you cannot find a better fit from its subclasses.

OutOfBoundsException

This exception is used when an invalid index is called. Not to be confused with OutOfRangeException, OutOfBoundsException is a run time exception.

For example, when a user creates an array data structure and when an invalid index is called, an OutOfBoundsException should be thrown. Whereas trying to obtain the day of week using 8 should throw an OutOfRangeException.

$booksList = array();
 
$bookList[5];     // OutOfBoundsException
 
$dayOfWeek = $calendar->day(8); // OutOfRangeException

OverflowException

When a container with limited capacity is asked to fill more than it can hold, this exception can be thrown.

RangeException

This exception is for generic error cases related to range at "run time".

UnderflowException

Opposite of OverflowException is UnderflowException. When an empty container is asked to remove an element, this exception can be thrown.

UnexpectedValueException

As simple as its name says, when an unexpected value is raised or accessed, we throw this exception.

That is all the exceptions provided by PHP SPL. We should always throw the most accurate exception for an error case. Inevitably, one exception may fit under multiple exceptions in which case, it is fine to pick one.

An meaningful exception message goes a long way towards a maintainable project.

The End

Hopefully this simple tutorial helped you with your development. If you like our post, please follow us on Twitter and help spread the word. We need your support to continue. Did we miss out anything? Do leave a comment below to let us know.