Lesson 87 of 100
Errors15 min5 checksPHP 8.5

Define Domain-Specific Exceptions

Learn to extend Exception with your own classes, carry codes and previous errors, and catch them precisely so callers can react to specific failures.

01 · THE IDEAREAD → TRY → LEARN

Why custom exceptions?

PHP's Exception is the base class for errors that can be handled during normal operation. Throwing a plain Exception works, but every caller then has to read the message text to find out what went wrong. A custom class gives the failure a name. Code can catch OutOfStockException and ignore everything else, and the class name documents the problem on its own.

A custom exception is just a class that extends Exception (or a more specific built-in such as RuntimeException or InvalidArgumentException). The class body can be empty, because the constructor, getMessage(), getCode() and getPrevious() are inherited. Those getters are final, so you cannot override them, but you can add your own methods and properties.

Adding your own data

The inherited constructor takes string $message = "", int $code = 0 and ?Throwable $previous = null. When you write your own constructor, call parent::__construct() so the message and code are stored. The example below keeps a SKU and builds a consistent message from it.

Example
<?php
final class OutOfStockException extends RuntimeException
{
    public function __construct(private string $sku, ?Throwable $previous = null)
    {
        parent::__construct("Out of stock: $sku", 409, $previous);
    }

    public function sku(): string
    {
        return $this->sku;
    }
}

try {
    throw new OutOfStockException('A-100');
} catch (OutOfStockException $e) {
    echo json_encode([$e->getMessage(), $e->getCode(), $e->sku()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
}
Output
["Out of stock: A-100",409,"A-100"]

Building a hierarchy

Catch blocks match by class, and a parent class matches all of its children. A shared base such as ShopException lets a caller handle every shop-related failure with one block, while more specific classes remain available when finer control is needed. Put the more specific catch first if you have several.

Example
<?php
class ShopException extends Exception {}
class PriceException extends ShopException {}

function setPrice(int $cents): int
{
    if ($cents <= 0) {
        throw new PriceException('Price must be positive');
    }
    return $cents;
}

try {
    setPrice(0);
} catch (ShopException $e) {
    echo get_class($e), '|', $e->getMessage(), "\n";
}
Output
PriceException|Price must be positive

Wrapping lower-level errors

When a low-level failure happens, translate it into your domain language but keep the original as the previous exception. The cause is then still available through getPrevious() for debugging.

Example
<?php
class ConfigException extends Exception {}

function loadConfig(string $json): array
{
    try {
        return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new ConfigException('Invalid config', 100, $e);
    }
}

try {
    loadConfig('{bad');
} catch (ConfigException $e) {
    echo $e->getMessage(), '|', $e->getCode(), '|', get_class($e->getPrevious()), "\n";
}
Output
Invalid config|100|JsonException

Practical guidelines

  • Name classes after the problem, not the place: InvalidQuantityException, not CartError2.
  • Use the code for a stable machine-readable value and the message for humans.
  • Extend the built-in class closest in meaning, so generic handlers still work.
  • Catch only what you can handle. Let other exceptions travel upward.
  • Never use exceptions for ordinary control flow that an if can express clearly.

Exercise

Write checkQuantity(int $quantity, int $stock): array. Inside the function, throw an InvalidQuantityException when the request is invalid, then catch it in the same function and convert it into a plain array.

  • If $quantity is below 1, throw with message Quantity must be at least 1 and code 1.
  • If $quantity is greater than $stock, throw with message Only N in stock (N is the stock) and code 2.
  • Otherwise return ['ok' => true, 'quantity' => $quantity].
  • On failure return ['ok' => false, 'code' => ..., 'message' => ...].

Pay attention to the boundaries: a quantity equal to the stock is allowed, and a quantity of 0 is not.

Before you start

Functions · Arrays · Types

You’ll practice
  • Throw and catch a custom exception within the wrapper
  • Implement and test a deterministic PHP 8.4 function
Your task

Make checkQuantity() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.