Skip to content
Theory · Errors and exceptions · lesson 4 of 8

The Throwable hierarchy

The block catch (Exception $e) catches Exception and its child classes. It seems that should be enough for any failure. Let's check it on an error that PHP itself throws:

PHP
<?php

function stars(int $count): string
{
    try {
        return str_repeat('*', $count);
    } catch (Exception $e) {
        return '';
    }
}

echo stars(3), "\n"; // ***
echo stars(-1), "\n"; // ✗
TEXT
Fatal error: Uncaught ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0

The first call printed ***. In the second, str_repeat got a negative number and threw a ValueError. The catch block did not catch it, and the program stopped.

The courier's parcels come in two kinds: exceptions come from your code and libraries, and errors come from the PHP engine. Both go up the same stairs, but a door labeled Exception opens only for exceptions. An error parcel needs an Error or a Throwable door.

Two branches under Throwable

Everything PHP can throw implements the Throwable interface. Below it there are two branches, and they do not overlap.

The Exception branch

Exceptions are thrown by your code and by libraries when something goes wrong in the program's work:

  • Exception—the general class of the whole branch;
  • InvalidArgumentException—a function was called with a wrong argument; its parent is LogicException, "a mistake in the logic of the call";
  • RuntimeException—a failure you can only see while running: a missing file, a full disk.

The Error branch

Errors are thrown by the engine when it cannot run a line itself:

  • TypeError—an argument of the wrong type; its descendant ArgumentCountError—too few arguments;
  • ValueError—the type is right but the value is not, like -1 in str_repeat;
  • DivisionByZeroError—division by zero;
  • Error—the general class of the branch, for example on a call to a function that does not exist.

A catch catches its own class and that class's descendants, and Error is not a descendant of Exception. That is why catch (Exception $e) never catches an Error. How Exception differs from Error is a classic interview question, and this is the answer: they are two separate branches under Throwable.

Here the door picture stops working. A real courier could hand a parcel to anyone, while a catch checks the class, and the door never opens for the other branch.

How to catch an Error

To catch an engine error, name its class in the catch:

PHP
<?php

function stars(int $count): string
{
    try {
        return str_repeat('*', $count);
    } catch (ValueError $e) {
        return '';
    }
}

echo '[', stars(-1), ']', "\n"; // []

Now stars(-1) returns an empty string. More often, though, an engine error means that the code itself is wrong. Then it is better to fix the code—for example, check $count before the call—than to hide the error in a catch.

Throwable at the outer boundary

catch (Throwable $e) catches both branches. It is placed once, at the outer boundary of the program, around everything the program does. There it shows a clear message instead of an empty page, and the details can be written to a log; in the example, echo prints them:

PHP
<?php

function report(): void
{
    echo 'counting', "\n"; // counting
    echo intdiv(1, 0), "\n";
}

try {
    report();
} catch (Throwable $e) {
    echo 'Sorry, something broke', "\n"; // Sorry, something broke
    echo get_class($e), "\n"; // DivisionByZeroError
}

The get_class function returns the class name of an object, and that name shows what exactly happened. Deep inside a program, catch (Throwable $e) is not used: it would also catch a mistake in the code, which needs fixing, not silencing.

Common mistake

A class meant for throwing is made by implementing the Throwable interface directly:

PHP
<?php

class PaymentFailed implements Throwable {} // ✗
TEXT
Fatal error: Class PaymentFailed cannot implement interface Throwable, extend Exception or Error instead

PHP does not let your own class implement Throwable. A class becomes throwable only through inheritance, as the message itself suggests: class PaymentFailed extends Exception {}.

Key points

  • Throwable is an interface with two separate branches below it: Exception for the program's failures and Error for engine errors.
  • catch (Exception $e) does not catch TypeError, ValueError or DivisionByZeroError: they are in the Error branch.
  • An engine error is caught by its class, but more often the code that caused it is fixed.
  • catch (Throwable $e) catches everything and goes once, at the outer boundary of the program.