Skip to Content
Course content

118: The Exception Class Hierarchy in PHP

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you're comfortable wrapping risky code in a try-catch block. But if you look at a lot of legacy PHP projects, you'll see a pattern that I call "The Catch-All Vacuum." It's the habit of throwing the base \Exception class for every single thing that goes wrong. When you're first starting out, it feels efficient. Why bother creating five different classes when throw new \Exception("Invalid API key") does the trick? The problem is that you're throwing away the most valuable part of an exception: its type.

The Trap of the Generic Exception

Imagine we're building a payment processing module. If you use the naive approach, your code probably looks something like this:

function processPayment($amount, $cardDetails) {
    if (!$cardDetails->isValid()) {
        throw new \Exception("Invalid card details");
    }
    if ($amount > $cardDetails->getBalance()) {
        throw new \Exception("Insufficient funds");
    }
    if (!PaymentGateway::ping()) {
        throw new \Exception("Gateway timeout");
    }
}

At first glance, this is fine. But look at it from the perspective of the code calling this function. If you want to handle a "balance too low" error differently than a "server is down" error, you're forced to do something truly hideous: you have to catch the exception and then perform string matching on the message.

try {
    processPayment(100, $card);
} catch (\Exception $e) {
    if ($e->getMessage() === "Insufficient funds") {
        // Ask user for a different card
    } elseif ($e->getMessage() === "Gateway timeout") {
        // Retry in 5 seconds
    } else {
        // Log a critical system failure
    }
}

I can't stress enough how fragile this is. The moment a teammate changes "Insufficient funds" to "Insufficient balance" in the processPayment function, your error handling silently breaks. You've turned your type-safe language into a guessing game of string comparisons.

Categorizing Errors for Better Control

The professional way to handle this is to leverage the Exception hierarchy. PHP provides several built-in SPL (Standard PHP Library) exceptions, but for your own business logic, you should build your own tree. I usually start by creating a base exception for the entire module, and then specialize from there.

In our payment example, we'd create a PaymentException that extends the base \Exception. Then, we create specific children for different categories of failure. For instance, PaymentValidationException (for user errors) and PaymentNetworkException (for infrastructure errors).

class PaymentException extends \Exception {}
class PaymentValidationException extends PaymentException {}
class PaymentNetworkException extends PaymentException {}

function processPayment($amount, $cardDetails) {
    if (!$cardDetails->isValid()) {
        throw new PaymentValidationException("Invalid card details");
    }
    if ($amount > $cardDetails->getBalance()) {
        throw new PaymentValidationException("Insufficient funds");
    }
    if (!PaymentGateway::ping()) {
        throw new PaymentNetworkException("Gateway timeout");
    }
}

Now, the hierarchy does the heavy lifting for you. You can catch the most specific error first, and let the more general ones fall through to a broader handler. It's much cleaner, and it's completely immune to changes in the error message text.

Where the Architecture Actually Wins

You might be thinking, "This is just more files to manage." And you're right; it is. But the trade-off is a massive gain in architectural clarity. Look at how the calling code transforms:

try {
    processPayment(100, $card);
} catch (PaymentValidationException $e) {
    // We know this is a user-fixable error. Show a friendly message.
    echo "Please check your payment details: " . $e->getMessage();
} catch (PaymentNetworkException $e) {
    // This is a technical glitch. Log it and suggest a retry.
    Log::error($e->getMessage());
    echo "Our payment provider is struggling. Try again in a moment.";
} catch (PaymentException $e) {
    // This catches any other payment-related error we didn't specifically target.
    echo "A payment error occurred.";
} catch (\Throwable $e) {
    // This is the ultimate safety net for non-payment crashes (like TypeErrors).
    echo "A critical system error occurred.";
}

By using a hierarchy, you've created a filtering system. You've separated "user mistakes" from "system failures" without ever having to read a single string of text. This is how you build a system that doesn't collapse the moment you decide to translate your error messages into another language or tweak a typo.




📋 Practical Task

Exercise: Implementing a Tiered Library Exception Hierarchy

You are building a File Management Library. Currently, the library throws generic \Exception objects for everything, making it impossible for the implementing developer to distinguish between a file that is missing and a file that the system doesn't have permission to read.

Your Task:

  • Create a base exception class called FileLibException.
  • Create two specialized child classes: FileNotFoundException and FilePermissionException.
  • Write a function readFileContents($path) that:
    • Throws a FileNotFoundException if the file does not exist (use file_exists()).
    • Throws a FilePermissionException if the file exists but is not readable (use is_readable()).
    • Returns the file contents if both checks pass.
  • Implement a try-catch block that handles FileNotFoundException by suggesting a default file path, handles FilePermissionException by logging a security warning, and handles all other FileLibException errors with a generic "File system error" message.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.