Let’s examine the best ways of setting them up and use them in PHP, and how to avoid creating a mess while doing so.
Why Even Use Exceptions?
Before discussing implementation details, I think it is necessary to mention what the main benefit of exceptions is when compared to something like trigger_error().
Normal PHP errors and warnings come from the procedural world, and they provide one central mechanism to deal with these errors, a function you declare as being the error handler through the set_error_handler() method. Apart from being able to set different error handlers for different levels of errors, you have no further way of controlling the point where you can deal with your errors. The error handler is somewhat outside of the context of your current execution flow, so it is very difficult to do anything meaningful to recover from the error once you’ve reached that point.
Exceptions, on the other hand, can be dealt with at any point in your code. And, as they escalate throughout your different layers of code, you can decide for each exception at which layer and in which context you want to deal with them. This allows you to catch and gracefully handle exceptions directly in the calling code when this makes sense, and let them bubble to the surface if not.
As an example, when your code that fetches an entry from the database throws an exception, you can handle this in a nuanced way based on the type of the exception. Did the database not return a result because that ID was not known? Maybe return a NullObject instead of throwing an error. This will let a user continue working with the system without getting fatal errors. If the connection could not be established, however, we might want to have our exception bubble up to the infrastructure layer to immediately point to a bigger issue.
Build Upon SPL Exceptions
The Standard PHP Library (SPL) provides a predefined set of exceptions that I would recommend you to build upon. These provide a generalized classification and having your own exceptions extend these makes it easier for consuming code to catch them. The consuming code can, for example, choose to catch the general group of RuntimeException, which would include standard PHP exceptions as well as your own extensions of this.
For my own components, I have a standard set of exception groupings I pull in via Composer that mirrors these SPL exceptions while adding an interface to them that is specific to my component’s organization, which is called a “Marker Interface”. All of my custom exceptions then extend one of these. The point of adding this additional layer of exceptions is to have one additional way of filtering what exceptions I want to catch. I found this to be needed when you work in a codebase where project code is built on a framework, and that framework is built on individual packages.
Every base exception in that standard set both extends one of the SPL exceptions as well as implements the marker interface. This then effectively gives you a way of only catching “Framework-specific” exceptions from within any of your code, by targeting that marker interface.
The following is a quick summary of how to catch exceptions at different granularities.
Catch all exceptions: catch( Exception $exception ) {} Catch all exceptions thrown by your organization's library: catch( MyOrganization\Exception\ExceptionInterface $exception ) {} Catch a specific SPL-based exception (By your organization or not): catch( LogicException $exception ) Catch a specific SPL-based exception thrown by your organization's library: catch( MyOrganization\Exception\LogicException $exception ) {}
As you can see, we cover layer-specific catching as well as type-specific catching with such a structure, and we didn’t even need to catch actual specific exceptions to do so.
Naming Conventions
My current take on this is to name the exceptions differently based on whether they represent a specific error to be acted on or a group that other exceptions should extend to further qualify.
So, as an example, consider having a piece of code that tries to load the contents of a file, and that has three different ways of failing:
The file name is not valid. The file was not found. The file is not readable.
You could build the following exceptions to make this semantically clear:
FileNameWasNotValid extends InvalidArgumentException FileWasNotFound extends InvalidArgumentException FileWasNotReadable extends RuntimeException
The groups have the Exception suffix, as their naming reflects a “group of exceptions”. The specific exceptions that need to be thrown don’t include that suffix, though, but rather are formed by building a past-tense sentence from the error condition that has happened.
When exceptions are named in this manner, together with the concept explained in the next sections, reading the code becomes much clearer, as you are almost reading normal sentences.
Named Constructors Encapsulate Exception Logic
Most developers tend to construct the exceptions right where it happens. This often results in methods where the exception-throwing part has more lines of code than the actual logic. Reading code like that is very cumbersome, as it requires substantial mental effort to identify what the important statements are.
I recommend building named constructors for your exceptions, so that the actual operation of preparing the arguments for instantiating the exception are encapsulated within the exception code, and do not pollute your business logic more than necessary.
As an example, let’s consider the piece of code in Listing 1.
Listing 1
public function render( $view ): string { if ( ! $this->views->has( $view ) ) { switch ( gettype( $view ) ) { case 'object': $view = get_class( $view ); case 'string': $message = sprintf( 'The requested View "%s" does not exist.', $view ); break; default: $message = sprintf( 'An unknown View type of "%s" was requested.', $view ); } throw new ViewWasNotFound( $message ); } echo $this->views->get( $view ) ->render(); }
As you can see, the code for dealing with the exception is more complicated than the actual logic. To remedy this, include named constructors in your exceptions, like the Listing 2.
Listing 2
final class ViewWasNotFound extends InvalidArgumentException { public static function fromView( string $view, int $code = 0, Exception $previous = null ): self { switch ( gettype( $view ) ) { case 'object': $view = get_class( $view ); case 'string': $message = sprintf( 'The requested View "%s" does not exist.', $view ); break; default: $message = sprintf( 'An unknown View type of "%s" was requested.', $view ); } return new static( $message, $code, $previous ); } }
You can have multiple named constructors within the same exception, depending on what context they should be instantiated in. Basically, you’ll have one named constructor for each distinct message.
Now, rewriting the previous business logic to use this named constructor makes the code much cleaner:
public function render( $view ): string { if ( ! $this->views->has( $view ) ) { throw ViewWasNotFound::fromView( $view ); } echo $this->views->get( $view ) ->render(); }
Much nicer, don’t you think? Apart from the code being cleaner to read, we’ve also put the actual string to use as a message within the Exception itself, which is easier to maintain.
Localized Exception Messages
In some instances, it might make sense to allow your exception messages to be localized through gettext(). However, I would advise against doing this as a general habit, as it comes with disadvantages as well.
Localizing the exception messages makes sense for a group of exceptions that is meant to be directly used to provide feedback to the user at the front end.
Although exceptions should not generally be directly used at the front end (since they should be exceptional, by their very nature), this sometimes might make sense. In such a case, localizing your exceptions is easily done by wrapping the string within your named constructors into gettext() functions.
The main disadvantage is that exceptions are often searched for through their message string, by developers/sysops in log files, and by users in search engines. Localized messages break such a search strategy. If no other identifier is provided (exception name, exception code), it is very difficult, especially for users, to find out what is going on.
Therefore, my recommendation is to never use localized message strings in exceptions, and if you have user feedback that needs to be localized based on error conditions, use custom-built Error objects for this instead. This also clearly distinguishes between exception errors (“the database is down”) and expected user input errors (“the entered email is not valid”).
Catching Exceptions
It sounds very obvious, but it is actually pretty difficult to do in practice: Only catch the exceptions you can directly handle within your current context.
So, if your intent is to do an operation in a fail-safe way, and return a NullObject in case something goes south, it is perfectly okay to just generally catch( Exception $exception ) {}. However, if you are trying to execute an operation that needs to be completed successfully to maintain the integrity of your system, pay close attention to only catch the exceptions that you are actually able to properly remedy. Otherwise, you might prevent a more severe exception to bubble up to the top, and effectively “hiding” an issue within your system.
In most cases, you should differentiate between logic exceptions and runtime exceptions. That’s why the SPL exceptions we’ve seen before provide these two types as their upper-most level of distinction.
Logic exceptions are exceptions where you as a developer are currently doing something wrong. You are asking for values that cannot exist, call methods with the wrong type of argument, etc… This is the type of stuff that needs to be dealt with immediately during development. In a perfect world, a logic exception would never make its way to the production environment.
A runtime exception, however, is an error condition that you have no control over during development. A database connection getting blocked by the firewall, a file permission that is incorrect, etc… These errors are unavoidable. You can not “develop a system where the database cannot fail”. What you can do is “develop a system where a failed database connection produces a nice error message and alerts the system administrators, instead of throwing a fatal error”. I.e., your code cannot ‘never fail’, but it can ‘only ever fail gracefully’.
This is why you should make sure to catch runtime exceptions at some layer in your system (the exact layer depends on many factors) and let logic exceptions pass through so that they throw obvious error messages for the developer.
Keep in mind that you can provide multiple catch clauses, and an exception will be compared against each of them in order as long as no match was found yet.
Also, starting with PHP 7, a new interface Throwable was added that regroups both Exception and Error instances. Error objects are throwable and can be caught just like Exception objects, but they represent internal PHP errors, like a ParseError.
If you intend to use the Throwable interface but still support PHP 5 code, you can stack them, you just have to make sure to catch the Throwable first and then fall back to the Exception:
try { // Do something here. } catch ( Throwable $exception ) { // This catches both exceptions and errors in PHP 7+. } catch ( Exception $exception ) { // This catches only exceptions in PHP 5 (errors don't exist yet). }
Rethrow When Crossing Layer Boundaries
In a more complex codebase, you’ll end up having several different layers, like the business domain, the presentation layer, etc… Best practice is to always catch any unhandled exceptions when leaving a layer and rethrow them as a new exception for the layer you’re entering.
I’ll try to explain this rather abstract principle via an example. Imagine the following: your application tries to load an image that was meant to be previously uploaded by a user to display it on a page. The application layer will request the image from the business domain layer, and the business domain layer will fetch it from the infrastructure layer. Only the business domain layer should know where that particular image is stored, and only the infrastructure layer should know how that particular image is stored.
While the infrastructure code tries to load the image, it can hit many different error conditions: the file does not exist, the file is not readable, the file is corrupted, the filesystem is not available, the system ran out of memory, … Each of these should throw a different exception so that we know what is going on and collect the corresponding contextual data.
When the business domain layer code tries to fetch that image from the infrastructure code, it will catch exceptions, as this is an operation that cannot be guaranteed to work (any I/O operations fall into that category). It would be bad form to let these exceptions go uncaught and just take down the server, and just logging them in the central exception handler is also not enough. What we want to happen is that the application layer prepares feedback for the user and forwards it to the presentation layer.
Instead of having the application layer deal with hundreds of exception types for all the subsystems and layers, the exception handling itself should be layered as well and respect the layer boundaries.
So the business domain layer would catch all possible exceptions from the infrastructure layer that are directly related to a problem with loading the image file. Then it would try to make as much sense as possible out of these and rethrow a new exception that originates from the business layer instead.
So we could have something like the following happen in terms of exception handling:
Fig. 1: Exception Handling
This way, every layer reduces the complexity of the error handling again to a required minimum and only deals with the knowledge that is needed at any given depth.
Central Error Handler
To not just take the webserver down for the exceptions you let through (whether that was intended or not), you should provide a central error handler that catches any remaining exceptions that haven’t been caught up to that point.
At the very least, you should log these exceptions and their stack trace, so that you can analyze what has happened after the fact.
I recommend using a library like thephpleague/booboo to wrap your entire system into such a central error handler.
By doing so, you can ensure that any exception that was thrown within your system will:
- either be handled by a catch clause in one or more of the layers of your application;
- or be logged together with contextual information so you can be notified of the uncaught exception and debug it.
Conclusion
The above points should help you keep a certain structure to the way you deal with error conditions within your codebase that is easy to grow and maintain. As with most object-oriented concepts, it will take some practice to properly internalize these principles and use them naturally. Hopefully, this article helps you avoid the majority of pitfalls while getting to grips with them. Have exceptional fun testing this out on your code!