How to use built-in SPL exception classes for better error handling

Tags:

Since PHP 5, there has been a bundle of built-in exceptions – the “SPL exceptions” – in PHP. However, the documentation for these classes is quite lacking in examples, and it can be difficult to understand when you should be using them.

The short answer is always. You’ll find a longer answer if you continue reading :)

There is also a french translation of this post, by Frédéric Blanc.

What are the SPL exceptions

The very base PHP exception class is the Exception.

The SPL exceptions are quite similar to this base class, but they are more specialized – as in, they should be used to report more specific error conditions. They are used exactly the same as the base exception – just throw.

You should be using these more specific exception classes most of the time, as they fit the more specific conditions you typically have quite well.

You can find the list of SPL exceptions in the PHP manual. Next, let’s look at each of them and an example or two of how to use them.

BadFunctionCallException

Going in the same order as they are in the manual, the first is BadFunctionCallException.

This exception isn’t very useful in my opinion. Generally this may get thrown by PHP if you’re calling some code incorrectly, but since the main use for this is if a function (not a method in a class) is called without all the parameters or if the function doesn’t exist, custom code rarely needs this.

BadMethodCallException

This one is a bit more useful. Similar to BadFunctionCallException, this should be thrown when a class method either does not exist or does not have all the required parameters.

The main use for this exception is when you implement the __call magic method:

public function __call($name, $arg) {
    //Let's say here's a condition which determines what to do
    if(...) {
        //This is the succesful condition and perhaps we return a value or something
    }
 
    //Here things went wrong - The method doesn't exist!
    throw new BadMethodCallException("The method '$name' does not exist"); 
}

This is in fact a best practice: Always throw a BadMethodCallException when you create a __call method! Otherwise your code could be calling functions which indeed don’t exist, and you never know about it (for example if you have typed the name wrong).

DomainException

The DomainException is a bit trickier case to explain.

Basically, this is what you would throw if your code messes up and for example a sanity-check finds a value is “outside the domain”.

For example, if you have a method which performs weekday calculations, and for some reason a result of a calculation is outside the 1-7 range (for days in a week), you could throw a DomainException. This is because the value is outside the “domain” for day numbers in a week.

InvalidArgumentException

This one is pretty self-explanatory: Throw an InvalidArgumentException when your functions or methods receive arguments that are invalid.

For example, if your function requires a number but is instead given a string, throw a InvalidArgumentException stating that the function requires a number:

public function numberRobot($number) {
    if(!is_numeric($number)) {
        throw new InvalidArgumentException('The number robot demands a numeric sacrifice!');
    }
}

Additionally, you can use this when the application receives invalid POST or GET arguments. For example, depending on how you want to handle your errors, you could throw an InvalidArgumntException in a controller which expects specific GET/POST arguments but gets wrong types of them.

LengthException

A LengthException can be used when the length of something is too short or too long – For example, a file name’s length could be too long.

This can also be applied if an array’s length is incorrect.

LogicException

The LogicException is again a bit trickier one because it has no obvious uses, as most cases are covered by many of the other exception classes.

The main use for LogicException is a bit similar to DomainException – it should be used if your code (for example a calculation) produces a value that it shouldn’t produce.

Errors which cause a LogicException to be thrown would generally be caused by bugs in your code.

OutOfBoundsException

An OutOfBoundsException should be thrown if code is attempting to access an invalid key.

Typically this could be used in code that attempts to access an associative array, but performs a check for the key.

Also, another use for this can be when you implement ArrayAccess in your class.

public function offsetGet($offset) {
    if(!isset($this->objects[$offset])) {
        throw new OutOfBoundsException("The offset '$offset' is out of bounds");
    }
 
    return $this->objects[$offset];
}

Note: This should be used for keys, not indexes (as in strings, not numbers). You may wish to check when implementing whether or not the offset being read is a number or not, and throw a OutOfRangeException instead.

OutOfRangeException

This is the same as OutOfBoundsException, but this should be used for normal arrays which are indexed by number, not by key.

OverflowException

If your class acts like a container, you can use OverflowException when the object is full, but someone is trying to add more items into it.

RangeException

This is an exception that should be thrown when a value is out of some specific range. It’s similar to DomainException in its intended purpose, but it should be used in cases where the error is going to be caused in a runtime scenario.

RuntimeException

The purpose of the RuntimeException is somewhat similar to as its namesake’s purpose is in Java.

In Java-world, you have checked and runtime exceptions. Checked exceptions must always be caught – The Java compiler will not compile code which does not have catch-blocks for any code that throws checked exceptions.

Runtime exceptions in Java do not require a catch-block in the calling code.

Since PHP does not have support for checked exceptions, the divide between runtime and other exceptions is less strict. However, the purpose of a RuntimeException is still similar: It should be throw in cases where the calling code does not necessarily have the capacity to handle it.

This also applies to subclasses of RuntimeException: OutOfBoundsException, OverflowException, RangeException, UnderflowException and UnexpectedValueException.

UnderflowException

This is the opposite of OverflowException – If your class is a container and it’s empty, but someone is trying to remove elements from it, you can throw a UnderflowException.

UnexpectedValueException

A UnexpectedValueException should be thrown if a value is outside a set of values.

For example, if you have a list of const‘s, and a value must be one of them:

const TYPE_FOO = 'foo';
const TYPE_BAR = 'bar';
 
public function doSomething($x) {
    if($x != self::TYPE_FOO || $x != self::TYPE_BAR) {
        throw new UnexpectedValueException('Parameter must be one of the TYPE_* constants');
    }
}

Other tips

Sometimes more than one exception will look like it suits the error. In this case, you should always try to use the exception which most accurately explains what went wrong – The goal should be to make debugging the code easy.

This is also why you should always try to make the exception’s message be helpful. It doesn’t need to be understandable by whoever is using your application, but it should be understandable by whoever is going to be developing or maintaining it.

If you’re writing a library to be used by other programmers, the exceptions should also try to be as helpful as possible to the user of the library. It’s highly frustrating when you’re trying to figure out why some code doesn’t work, when it keeps producing confusing error messages.

Conclusion

You should always try to use the exception which best describes the error scenario your code is in. This does not only make handling the error easier, it also makes it easier to find and understand the cause of the error.

If you have any other uses for the SPL exception classes than what I have mentioned here, feel free to leave a comment – The official documentation for them is poor and I’m sure there are more ways to use them.

Further reading:
Should a failed function return a value or throw an exception? in this blog.
Exceptions and abstraction in this blog.
Brandon Savage has also written some good exceptions related articles on his blog.