Essay

October 24, 2016 · 9 min · Updated August 12, 2019

Structuring PHP Exceptions

While the consensus is to use exceptions instead of errors, there is very little information on how to structure and manage them in a larger codebase.

I seem to constantly work on improving my habits regarding the use of exceptions. I think it is an area that I haven’t yet fully explored, and it is very difficult to find anything more than very basic explanations and tutorials online. While the consensus is to use exceptions, there is very little information on how to structure and manage them in a larger codebase. The larger and more complex your projects become, the more important it is to start with a proper structure to avoid expensive refactoring later on. Your client will surely be thankful!

In large #PHP projects, start with a proper structure to avoid expensive refactoring later on.

In this article, I want to talk about the way I currently set them up and use them in PHP, in the hopes to spark some discussion on the topic and get further feedback.

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 you 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. 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 exceptions in that standard set both extends one of the SPL exceptions as well as implements the BrightNucleus\Exception\ExceptionInterface interface. This then effectively gives me a way of only catching “Framework-specific” exceptions targeting from within any of my code, by targeting that interface.

Here’s a quick summary of how to catch exceptions at different granularities:

  1. Catch all exceptions catch( Exception $exception ) {}
  2. Catch all exceptions thrown by a Bright Nucleus library catch( BrightNucleus\Exception\ExceptionInterface $exception ) {}
  3. Catch a specific SPL exception (BrightNucleus or not) catch( LogicException $exception )
  4. Catch a specific SPL exception thrown by a Bright Nucleus library catch( BrightNucleus\Exception\LogicException $exception ) {}

As you can see, we cover layer-specific catching as well as type-specific catching with such a structure.

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:

  1. The file name is not valid.
  2. The file was not found.
  3. The file is not readable.

I would 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 actually has more lines of code than the actual logic. Reading code like that is very cumbersome, as you need substantial 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.

Use Named Constructors for your #PHP exceptions to keep your business logic clean.

As an example, let’s consider this piece of code:

public function render( $view ) {

   if ( ! $this->views->has( $view ) ) {
      $message = sprintf(
         'The View "%s" does not exist.',
         json_encode( $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 following:

class ViewWasNotFound extends InvalidArgumentException {

   public static function fromView( $view, $code = 0, Exception $previous = null ) {
      $message = sprintf(
         'The View "%s" does not exist.',
         json_encode( $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 ) {

   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 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.

Localized exception messages will make it harder for your users to ‘google’ for solutions.

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 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.

Logic exceptions should never make their way to your 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”.

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.

Central Error Handler

To not just take the web server 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 something like BooBoo to wrap your entire system into such a central error handler.

Provide Your Insights

This was a quick run-down of how I am currently using exceptions. I don’t think I’ve properly mastered them yet, though. I welcome any feedback or insights on how you use exceptions in your projects and specific issues you’re facing. So please go ahead and join the discussion in the comments!


Update 2018-07-06: Changed the signature of the named constructor to use code = 0 instead of code = null, to avoid issues with strict type-checking. Kudos to Gary Jones for the feedback!

All essays