Skip to content

PHP Tutorial Error Handling

Custom Exceptions

Lesson 192 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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.

index.php
PHP
<?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();
}
Output
OUTPUT
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.

index.php
PHP
<?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";
    }
}
Output
OUTPUT
Other app problem: You may not open secret.
404: No page called missing.

Carry extra data

index.php
PHP
<?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());
}
Output
OUTPUT
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.

index.php
PHP
<?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();
}
Output
OUTPUT
File is not readable: report.pdf

Rules of thumb

  1. Extend Exception (or RuntimeException, LogicException).
  2. Name it for the problem, ending with Exception.
  3. Do not create dozens of classes. Start with a few that callers really catch differently.
  4. Keep messages free of secrets.
info
Why this matters in Laravel
Laravel and Symfony apps are full of small custom exceptions such as ModelNotFoundException, and the framework turns them into the right HTTP response. The same idea works in plain PHP.