100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
PHP

Exceptions and Error Handling

Learn how PHP represents runtime failures as throwable objects, how try/catch/finally blocks control the flow around them, and how to design a clean exception hierarchy for your application.

Error Handling & NamespacesIntermediate10 min readJul 9, 2026
Analogies

Exceptions and Error Handling

PHP treats most runtime failures as objects that implement the Throwable interface, rather than as return codes or silent warnings. When something goes wrong — a database connection drops, a required array key is missing, a file cannot be opened — code can throw an exception object that carries a message, a numeric code, and (crucially) the exact call stack at the moment of failure. Surrounding code can catch that object, inspect it, recover, log it, or let it propagate upward until something knows how to handle it. This model separates the 'happy path' of your business logic from the 'what if it fails' branches, which keeps functions readable and makes failure handling explicit rather than scattered through if-checks on return values.

🏏

Cricket analogy: When a batter gets a controversial LBW decision, the umpire doesn't just wave play on silently — he raises a decision object with a reason, letting the third umpire 'catch' it for review before it's accepted or overturned.

The Throwable hierarchy: Error vs Exception

Since PHP 7, both Error and Exception implement the Throwable interface, so a single catch block can intercept either if it type-hints Throwable. Error and its subclasses (TypeError, DivisionByZeroError, ArgumentCountError) represent internal engine failures — calling a function with the wrong argument types, dividing by zero, or invoking a method on null. Exception and its subclasses (InvalidArgumentException, RuntimeException, LogicException) represent application-level failures you throw deliberately. Catching Error is possible but should be rare and deliberate; most of the time you want your own code to raise typed Exception subclasses that describe a specific failure mode, and let genuine engine errors surface during development instead of being silently swallowed.

🏏

Cricket analogy: A no-ball called for overstepping is like an Error — the game's own engine flagged a rule violation — while a batter deliberately retiring hurt is like an Exception, a choice made within the normal flow for a specific reason.

php
<?php
declare(strict_types=1);

final class InsufficientFundsException extends RuntimeException
{
    public function __construct(
        public readonly float $requested,
        public readonly float $available,
    ) {
        parent::__construct(
            sprintf('Requested %.2f but only %.2f available', $requested, $available)
        );
    }
}

final class Account
{
    public function __construct(private float $balance) {}

    public function withdraw(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($amount, $this->balance);
        }
        $this->balance -= $amount;
    }
}

$account = new Account(50.00);

try {
    $account->withdraw(75.00);
} catch (InsufficientFundsException $e) {
    echo "Denied: {$e->getMessage()}\n";
    echo "Short by " . ($e->requested - $e->available) . "\n";
} finally {
    echo "Withdrawal attempt logged.\n";
}

try, catch, finally and multi-catch

A try block wraps code that might throw. One or more catch blocks follow, each matching a specific Throwable type (or a union of types separated by pipes, e.g. catch (TypeError | ValueError $e)). PHP checks catch blocks in order and runs the first one whose type matches, so list more specific exception types before their parent classes. The optional finally block always executes — whether the try succeeded, an exception was caught, or an exception was never caught at all and is about to propagate — making it the right place to release resources like file handles or database connections regardless of outcome.

🏏

Cricket analogy: A team's rain-delay protocol tries to continue play, has ordered responses from 'light drizzle' to 'washout,' and always runs the ground-covering procedure afterward regardless of which scenario occurred, protecting the pitch either way.

Think of an exception as a package thrown up a staircase. Each catch block on the way up gets to look at the label and decide whether to open it (handle it) or let it keep flying to the next landing. finally is the doorman at every landing who always does his job — checking coats, closing doors — no matter what happens to the package.

Custom exception hierarchies and rethrowing

Well-designed applications define their own exception classes that extend built-in ones like RuntimeException (failures detectable only at runtime) or LogicException (programmer errors that should never happen if the code is correct, like calling a method before initialization). Grouping related exceptions under a common application-specific base class — for example, an abstract PaymentException that InsufficientFundsException and CardDeclinedException both extend — lets calling code catch broadly ('any payment problem') or narrowly ('specifically insufficient funds') as needed. When you catch an exception only to add context before re-raising it, pass the original as the previous argument (the third constructor argument on Exception) so getPrevious() preserves the full causal chain for debugging.

🏏

Cricket analogy: A cricket board might classify rain interruptions and bad-light stoppages both under a broader 'PlayStoppageException,' letting officials react broadly or specifically, and note the original weather report as the 'previous' cause when escalating to the match referee.

A bare catch (Exception $e) that just does nothing (an empty catch body) silently destroys evidence of a bug. At minimum, log the exception. Swallowing exceptions without a trace is one of the most common causes of 'it just doesn't work and nobody knows why' production incidents.

Custom error and exception handlers

Beyond try/catch, PHP lets you install global hooks: set_exception_handler() catches any Throwable that escapes every catch block in the program (your last line of defense, typically used to log the error and show a generic message to users), and set_error_handler() intercepts traditional PHP errors and warnings (like accessing an undefined array index) so they can be converted into ErrorException objects and handled uniformly alongside real exceptions. Combining both means your application has a single, consistent path for reporting every kind of failure instead of two parallel systems.

🏏

Cricket analogy: A stadium's final safety officer is the last line of defense who intervenes if every other steward misses a crowd issue, while a separate protocol converts even minor incidents, like a spilled drink alert, into the same formal incident-report format used for serious ones.

  • All exceptions and internal engine errors implement Throwable; Exception and Error are its two main branches.
  • try/catch/finally separates normal logic from failure handling; finally always runs, even during an uncaught exception.
  • Catch specific exception types before their parent types — PHP matches the first compatible catch block in order.
  • Custom exception classes should extend RuntimeException or LogicException and carry structured data (typed properties), not just a string message.
  • Pass the original exception as the 'previous' constructor argument when rethrowing to preserve the full causal chain.
  • set_exception_handler() and set_error_handler() give you a global safety net for anything that escapes local try/catch blocks.

Practice what you learned

Was this page helpful?

Topics covered

#PHP#PHPProgrammingStudyNotes#Programming#ExceptionsAndErrorHandling#Exceptions#Error#Handling#Throwable#ErrorHandling#StudyNotes#SkillVeris