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.
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";
}["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.
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";
}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.
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";
}Invalid config|100|JsonException
Practical guidelines
- Name classes after the problem, not the place:
InvalidQuantityException, notCartError2. - 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
ifcan 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
$quantityis below 1, throw with messageQuantity must be at least 1and code1. - If
$quantityis greater than$stock, throw with messageOnly N in stock(N is the stock) and code2. - 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.
Functions · Arrays · Types
- Throw and catch a custom exception within the wrapper
- Implement and test a deterministic PHP 8.4 function