In most object-oriented languages, exceptions are an extremely powerful mechanism for dealing with unexpected situations that arise when running your code. PHP has supported robust exception handling since PHP 7.0.

As you begin your programming journey, exceptions are a source of tremendous pain. Over time, you grow to appreciate the value they bring. In this article, you'll learn about PHP exceptions, how you can make the most out of their usage, and how to improve your application with the information gathered from occurrences of them once in production. PHP exception handling and error tracking are an important part of building and deploying web applications to production.

Ready to dig in? We'll start by exploring the basics of exceptions in PHP.

What is an exception?

Technically speaking, a PHP exception is an instance of the Exception class, which implements the Throwable interface.

The Throwable interface is unique because it's not publicly available, which means you can't have your own classes implement it. Specifically, they can't implement it directly. If you create a class that extends Exception, then yes, your class is implicitly implementing the Throwable interface.

The other aspect that makes the Throwable interface special is that only instances of classes implementing it can be used in conjunction with a throw statement, which is a special language construct used to break the execution of a program in a problematic state. However, unlike die or similar statements, throw gives you the opportunity to overcome the problem and move on or, at least, fail elegantly.

As in many other languages, you generally wrap code that might throw exceptions with a try...catch block (if they are to be handled).

For instance, you might have a straightforward method that looks something like this:

public function doSomethingVeryUsefulOrThrowException()
{
    if ($this->weirdConditionHappened()) {

        throw new Exception("Some weird condition happened");
    }

    $this->doSomethingVeryUseful();
}

If you were to call this method at some other point in your code, you would probably want to use something like the following structure:

try {
    $helperObject->doSomethingVeryUsefulOrThrowException();
} catch (Exception $exception) {
    $this->handleException($exception);
}

This way, you can clearly separate the happy path from the error-handling logic. The try...catch block gives you guardrails to safely catch the exception that the doSomethingVeryUsefulOrThrowException method might throw.

Occasionally, you may need to perform a series of tasks regardless of whether an exception is thrown. For these situations, you have the finally keyword. To complete the example from above, a good use of the finally block would look something like this:

public function myMethod() {
    try {
        $helperObject->doSomethingVeryUsefulOrThrowException();
    } catch (Exception $exception) {
        $this->handleException($exception);
    } finally {
        $this->completeProcess();
    }
}

This is the case when you need to release some allocated resources. For instance, if before the try you opened a shared file, and the process is supposed to keep on running for a long time, it would be a good idea to close the file as part of the finally clause.

When should an exception be thrown?

When you first start working with exceptions, it can be very tempting to throw them every time you find an error. Try to resist this urge. Instead, think of exceptions as errors that can't be anticipated. In other words, if it can be prevented, it shouldn't be an exception.

Let's use an example of a method requiring an email address as a parameter:

public function sendEmailTo(string $recipient)
{
    if (filter_var($recipient, FILTER_VALIDATE_EMAIL) === false) {
        throw new Exception("$recipient is not a valid email address");
    }
    $this->doSendEmail($recipient);
}

If you must use this method to send an email message to an address retrieved from the UI, you could rely on the method throwing an exception if the input is incorrect and write your code as follows:

try {
    $emailSender->sendEmailTo($_POST['email']);
} catch (Exception $exception) {
    echo $exception->getMessage();
}

However, this is not a good way to use exceptions. If you're using the first method, then there's not much you can do to prevent the exception from happening. However, in the second method, you could make the validation before calling the sendEmailTo method.

This is particularly important when the called method comes from code you don't own (e.g., a third-party library). You don't want to depend too much on external factors when developing long-lasting code.

What is the difference between exceptions and errors?

In the case of PHP, for a long time (since around PHP5.3), errors and exceptions were quite different animals. However, since version 7.0, they've been brought closer together under the umbrella of the Throwable interface.

While functions like trigger_error are kept in the language to maintain backward compatibility, exceptions or Error objects should be preferred.

How to create PHP custom exceptions

One interesting fact about Exceptions is the ability to create your own. It's really easy, too; all you have to do is create a class that extends the Exception class:

class MyException extends Exception {}

Why would you do that? Well, when defining your try...catch blocks, you can have more than one catch for the same try. Therefore, if you make a call to a method that could throw different exceptions given some previously unknown conditions, you might want to react differently depending on which one happens. It would look something like this:

public function myMethod()
{
    try {
        $this->methodThatThrowsDifferentExceptions();

        echo "Everything worked ok!";
    } catch (FirstException $exception) {
        echo "Things failed for reason one";
    } catch (SecondException $exception) {
        echo "Things failed for reason two";
    } catch (Exception $exception) {
        echo "Things failed for a reason different than one or two";
    }
}

PHP exception handling and propagation

There are basically two operations that can be done once an Exception is thrown:

  1. Handle it.
  2. Let it propagate.
  3. Throw it again.

The first is the case when you implement a catch around the call to a potentially problematic method.

Another way to deal with an exception is to do nothing at all. The code would look like this:

public function myMethod() {
    $helperObject->doSomethingVeryUsefulOrThrowException();
    $this->completeProcess();
}

If the method doSomethingVeryUsefulOrThrowException does throw an exception, since there's no try...catch around the call, the exception will simply go up to the immediate caller. Therefore, you could use the following:

public function originalMethod() {
    try {
        $this->myMethod();
    } catch (Exception $exception) {
        $this->handle($exception);   
    }
}

The call to $this->handle() would be executed in the context of originalMethod. The same would happen if originalMethod doesn't have its own try...catch; originalMethod's caller would be expected to handle the exception.

However, what if no one handles the exception? Well, in this case, the whole program will be terminated, and the exception message will be shown and saved to a log file or some other default behavior, depending on the PHP configuration.

Finally, there's a third way to deal with a thrown exception. It’s a mix between the first two: handle and propagate. What? How would that be? Let me illustrate with an example:

public function myMethod() {
    try {
        $helperObject->doSomethingVeryUsefulOrThrowException();
    } catch (Exception $exception) {
        $this->handleException($exception);

        throw $exception;
    }
}

In this scenario, myMethod is both taking care of the exception and letting the caller take a shot at it. Would this ever make sense? Let's dig a little deeper.

Do you handle or propagate exceptions?

This behavior poses the question of when you should handle exceptions and when you should let them propagate.

The basic answer to this question is that you should handle an exception at the point in the program where something useful can be done to recover from the problem.

The following is an example of something you shouldn't do, although it's unfortunately fairly common:

public function myMethod() {
    try {
        $helperObject->doSomethingVeryUsefulOrThrowException();
    } catch (Exception $exception) {
        $this->log($exception);

        throw $exception;
    }
}

In principle, there doesn't seem to be anything wrong with this, right? Well, what would happen if myMethod is at the bottom of, let's say, a five-level deep call stack?

Let's make matters a little worse; what if every method in the call chain took the exact same approach (e.g., logging the exception and throwing it back up)?

You'd end up with bloated logs that don't provide much useful information. Proper debugging in PHP requires useful exceptions.

You're probably thinking that it never makes sense to re-throw an exception. This isn't the case! Sometimes it makes sense to re-throw an exception. Let me give you another, perhaps useful, example:

public function myMethod() {
    try {
        $helperObject->doSomethingVeryUsefulOrThrowException();
    } catch (Exception $exception) {
        $this->addContext($exception);

        throw $exception;
    }
}

In this case, before throwing the exception back to the caller, myMethod is providing some extra information that will hopefully be important and useful to you when troubleshooting. But how do you get that information for troubleshooting?

How to leverage Honeybadger for exception handling

Once your application is prepared to throw useful exceptions, you'll need to handle them. Hopefully, you'll be able to catch them in time to try some other approach or let the user know that you're experiencing a problem and offer an alternative solution.

However, sometimes, this is not the case, especially when you just finished developing your application and it's still in beta. Even mature applications eventually run into uncaught exceptions that can cause serious problems for users.

In these situations, you'll want to get as much information as possible about exceptions that happen when users are interacting with your application.

Depending on how your PHP web application is configured, you'll find this information somewhere in a log file. The exception details will usually look something like this:

PHP Fatal error:  Uncaught Exception in script.php:35
Stack trace:
#0 script.php.php(19): MyClass->methodThatThrowsDifferentExceptions()
#1 script.php.php(43): MyClass->myMethod()
#2 {main}
  thrown in script.php.php on line 35

Now that's not very readable, is it?

You'd probably be in a much better position if the uncaught exceptions were presented to you in a nice UI like this one:

PHP exception handling with a message in Honeybadger's dashboard

This is what Honeybadger's error tracking dashboard looks like.

From this point, you can go into a much more detailed view, where you can get a lot of contextual information, add comments on the exception message, and even assign it to a team member to remedy:

Detailed view of the exception

This certainly beats cutting through cumbersome log files looking for a hint, right?

Exceptions are actually helpful!

PHP exception handling has come a long way, especially with the improvements introduced in PHP 7 and refined in PHP 8. As you've seen, exceptions are a powerful tool for building resilient, maintainable applications. When something does slip through the cracks, using an error tracking tool like Honeybadger ensures you’re the first to know and can fix the error quickly.

You don't have to tell Honeybadger to watch over exceptions. It will do so by default. Therefore, by incorporating Honeybadger into your project, you can rest assured that someone will be notified about exceptions in your code as soon as something bad happens, giving you the chance to proactively fix the issue and delight your customers.

Try Honeybadger for FREE

Honeybadger combines the best of error tracking and performance monitoring into one simple interface—with support for all the frameworks you use. It’s the best way to gain real-time insights into the health of your applications.
Start free trial
Easy 5-minute setup — No credit card required
author photo
Mauro Chojrin

Mauro has been working in IT since 1997, mostly teaching programming. He has many years of experience working with PHP and Symfony. He currently works as an independent consultant and trainer.

More articles by Mauro Chojrin