The built-in exceptions are general. For your own application, you can create custom exceptions that say exactly what went wrong. Callers can then catch that specific case.
The simplest custom exception
An empty class that extends Exception is enough.
<?php
class OutOfStockException extends Exception
{
}
function buy(string $item, int $stock): string
{
if ($stock === 0) {
throw new OutOfStockException("$item is out of stock.");
}
return "Bought $item";
}
try {
buy("Notebook", 0);
} catch (OutOfStockException $e) {
echo $e->getMessage();
}
Notebook is out of stock.
Why bother
- The class name documents the problem.
- Code can catch only that case and let others pass.
- You can attach extra data.
A base class for your application
A shared parent lets you catch “anything from my app” at once.
<?php
class AppException extends RuntimeException {}
class NotFoundException extends AppException {}
class PermissionDeniedException extends AppException {}
function openPage(string $slug): string
{
if ($slug === "secret") {
throw new PermissionDeniedException("You may not open $slug.");
}
throw new NotFoundException("No page called $slug.");
}
foreach (["secret", "missing"] as $slug) {
try {
openPage($slug);
} catch (NotFoundException $e) {
echo "404: " . $e->getMessage() . "\n";
} catch (AppException $e) {
echo "Other app problem: " . $e->getMessage() . "\n";
}
}
Other app problem: You may not open secret.
404: No page called missing.
Carry extra data
<?php
class ValidationException extends InvalidArgumentException
{
public function __construct(private array $errors)
{
parent::__construct("Validation failed: " . count($errors) . " error(s)");
}
public function errors(): array
{
return $this->errors;
}
}
try {
throw new ValidationException(["email" => "Invalid", "name" => "Required"]);
} catch (ValidationException $e) {
echo $e->getMessage() . "\n";
print_r($e->errors());
}
Validation failed: 2 error(s)
Array
(
[email] => Invalid
[name] => Required
)
The exception carries the whole error list, so the code that catches it can show each message next to its field.
Named static constructors
A static method can give meaningful names and messages.
<?php
class FileException extends RuntimeException
{
public static function notReadable(string $path): self
{
return new self("File is not readable: $path");
}
}
try {
throw FileException::notReadable("report.pdf");
} catch (FileException $e) {
echo $e->getMessage();
}
File is not readable: report.pdf
Rules of thumb
- Extend
Exception(orRuntimeException,LogicException). - Name it for the problem, ending with
Exception. - Do not create dozens of classes. Start with a few that callers really catch differently.
- Keep messages free of secrets.
Why this matters in Laravel
ModelNotFoundException, and the framework turns them into the right HTTP response. The same idea works in plain PHP.