Parse Errors, Runtime Errors, and the Throwable Hierarchy

Distinguish PHP's error severities (parse errors, fatal errors, warnings, notices, deprecations, user-triggered errors) from modern engine Errors and Exceptions, and understand how the Throwable interface unifies both under try/catch.

Before you can handle errors well, you need a map of what PHP actually calls an 'error.' Historically PHP distinguished parse-time failures, several severities of runtime error message (fatal, warning, notice, deprecated), and user-triggered messages via trigger_error(). Since PHP 7, most fatal error conditions were reworked into a proper object hierarchy rooted in the Throwable interface, which finally let catastrophic engine failures be caught with try/catch the same way exceptions always could. This lesson builds the mental model everything else in this module depends on.

Parse errors: PHP won't even run

A parse error happens when PHP's tokenizer/parser cannot make sense of your source code at all — a missing semicolon, an unclosed brace, a stray keyword. Because parsing happens before any code executes, a parse error means the script never runs, not even the lines before the mistake. As of PHP 7+, a parse error is thrown as a ParseError object (a subclass of Error), so in principle it is catchable — but only if the broken file is include()'d or require()'d from another script that is already running; the top-level entry script that itself fails to parse cannot be caught by anything, because nothing of yours is executing yet.

Parse errors are purely syntactic. if ($x = 5 { (missing closing paren) is a parse error. if ($x = 5) { echo undefinedFunc(); } is syntactically valid — it parses fine and fails later, at runtime, when undefinedFunc() is called.

Runtime severities: fatal, warning, notice, deprecated

Once code does start running, PHP's classic error reporting model uses severity levels (originally int constants like E_WARNING, E_NOTICE) for issues that are not — or historically were not — represented as catchable objects. A (classic) fatal error, such as calling a function that doesn't exist or exhausting memory, used to halt script execution immediately with no way to intervene; PHP 8 converted most of these into catchable Error objects instead (see below), but a few conditions, like running out of memory or exceeding max_execution_time, remain true, unrecoverable fatal errors that simply stop the script.

A warning (E_WARNING) reports something clearly wrong — passing the wrong argument type in a loose-typed context, a failed fopen() — but execution continues with the statement's result typically becoming null or false. A notice (E_NOTICE) flags something suspicious but often harmless, such as string-to-number comparisons producing surprising results; PHP 8 promoted several former notices (like accessing an undefined variable) to warnings because they are common bug sources worth surfacing loudly. A deprecation notice (E_DEPRECATED) warns that a language feature or function you are using will be removed in a future PHP version — it does not affect behavior today, but ignoring it means your code will break on some future upgrade.

• Parse error (ParseError, extends Error) — code cannot be parsed; nothing runs • Fatal error — historically un-catchable; PHP 8 converted most into catchable Error subclasses, but resource exhaustion (memory, timeout) is still a true unrecoverable fatal • Warning (E_WARNING) — something is wrong; execution continues with a degraded result • Notice (E_NOTICE) — something is suspicious; execution continues normally • Deprecated (E_DEPRECATED) — using a feature slated for removal in a future PHP version

User-triggered errors

Your own code can raise classic-style messages with trigger_error($message, $level), where $level is one of E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, or E_USER_DEPRECATED. This predates exceptions being idiomatic in PHP and is still occasionally used by libraries that want to warn about misuse without throwing (for example, warning that a parameter will become required in the next major version). In modern PHP 8.2+ code, throwing an Exception or a custom Throwable is almost always preferable to trigger_error() for anything the caller is expected to react to, because exceptions carry structured data and force explicit handling, whereas a triggered error is easy to silently ignore.

Engine Errors and Exceptions: the Throwable hierarchy

PHP 7 introduced the Throwable interface as the common ancestor of everything that can be thrown and caught with try/catch. Throwable has exactly two direct implementors: Error and Exception. Neither Error nor Exception extends the other — they are siblings under Throwable, by design, so you can write catch (Throwable $e) to catch absolutely anything thrown, or narrow to just one branch.

Error represents engine/internal failure conditions — programming mistakes the language itself detects, like calling a method on null, calling an undefined function, or a type error from a strict-typed function call. Its common subclasses are TypeError, ValueError, ArgumentCountError, DivisionByZeroError, and ParseError. Exception (and its child RuntimeException, LogicException, and dozens of SPL subclasses) represents conditions your own application logic detects and throws deliberately — invalid input, a failed business rule, a resource that couldn't be obtained. The convention (not an enforced rule) is: Error for 'this program has a bug,' Exception for 'this is an expected, recoverable failure condition.'

Catching by severity level in the Throwable hierarchy

PHP evaluates catch blocks top to bottom, so the most specific type must come first; a catch (Throwable $e) listed first would swallow everything below it and Error/Exception-specific catches would never run. DivisionByZeroError is thrown only for integer division by zero (intdiv(), the % operator, or the / operator in PHP 8+); it is unrelated to floating point division by 0.0, which produces INF or NAN, not an exception.

<?php
try {
    $result = 10 / 0; // DivisionByZeroError extends ArithmeticError extends Error
} catch (DivisionByZeroError $e) {
    echo "Specific: {$e->getMessage()}\n";
} catch (Error $e) {
    echo "General engine error: {$e->getMessage()}\n";
} catch (Throwable $e) {
    echo "Anything at all: {$e->getMessage()}\n";
}

// User-triggered classic-style warning (rarely needed in modern code)
trigger_error('This function will require a second argument in v3.0', E_USER_DEPRECATED);

Code Examples

With strict_types=1, passing an int where a string parameter is declared makes PHP throw a TypeError (an Error subclass) instead of silently converting the value. TypeError is part of the engine Error hierarchy because it represents a programming mistake — a contract violation — not an application-level business condition. Expected result: Caught a TypeError: greet(): Argument #1 ($name) must be of type string, int given, called in ...

PHP 8's throw expression lets throw appear anywhere an expression is valid, including inside a match arm — useful for exhaustively handling every case and explicitly rejecting anything unexpected. ValueError (introduced in PHP 8.0) is the engine's way of signaling that a value, though of the correct type, is semantically invalid. Expected result: integer unknown (Unsupported type: array)

JSON_THROW_ON_ERROR converts json_decode()'s old silent-failure-plus-null-return behavior into a proper JsonException. This example also shows a common, deliberate pattern: catching a low-level exception and wrapping it in a higher-level, more meaningful one while preserving the original via the 'previous' exception chain (covered fully in the next lesson). Expected result: InvalidArgumentException: Malformed JSON payload Caused by: JsonException flags: JSON_THROW_ON_ERROR

<?php
declare(strict_types=1);

function greet(string $name): string {
    return "Hello, {$name}!";
}

try {
    echo greet(42); // strict_types=1 makes this a TypeError, not silent coercion
} catch (TypeError $e) {
    echo "Caught a TypeError: " . $e->getMessage() . "\n";
}
<?php
function classify(mixed $value): string {
    try {
        return match (true) {
            is_int($value) => 'integer',
            is_string($value) => 'string',
            default => throw new ValueError('Unsupported type: ' . get_debug_type($value)),
        };
    } catch (ValueError $e) {
        return 'unknown (' . $e->getMessage() . ')';
    }
}

echo classify(42) . "\n";
echo classify([1, 2, 3]) . "\n";
<?php
function riskyParse(string $json): array {
    try {
        return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        // JsonException extends Exception, not Error: malformed input is an
        // application-level, expected failure mode -- not an engine bug.
        throw new InvalidArgumentException('Malformed JSON payload', previous: $e);
    }
}

try {
    riskyParse('{not valid json');
} catch (Throwable $e) {
    echo get_class($e) . ': ' . $e->getMessage() . "\n";
    echo 'Caused by: ' . get_class($e->getPrevious()) . "\n";
}

The Throwable hierarchy

Error and Exception are siblings under Throwable -- neither extends the other. catch (Throwable $e) catches everything; catch (Error $e) or catch (Exception $e) catches only that branch and its subclasses.

Throwable (interface)
                    /                      \
                Error                     Exception
               /  |   \                   /    |    \
      TypeError ValueError ArithmeticError  RuntimeException LogicException ...
         |            |         |                 |              |
  ArgumentCountError  |  DivisionByZeroError  OutOfRangeException  DomainException
                      |                        RangeException     InvalidArgumentException
                ParseError, AssertionError,             ...            LengthException
                UnhandledMatchError, etc.

Important Rules

A parse error prevents the whole script from running; it can only be caught if it occurs in a file included from an already-running script.

Error and Exception both implement Throwable but do not extend each other -- catch Throwable to catch anything.

By convention, Error represents engine/programmer-mistake conditions (TypeError, ValueError, DivisionByZeroError); Exception represents expected, application-level failure conditions.

Since PHP 8, many former silent notices (like undefined variable access) were promoted to warnings, and PHP 8 also made undefined-variable access throw a Warning rather than staying silent -- write code that doesn't rely on undefined variables rather than suppressing the warning.

trigger_error()/E_USER_* levels predate exceptions being idiomatic; prefer throwing a Throwable in new code so failures can't be silently ignored.

Where this shows up

A production PHP application typically installs a top-level Throwable handler (set_exception_handler) precisely because Error subclasses like TypeError can now surface from deep inside third-party code, and the application needs one place to log any uncaught Throwable and return a generic 500 response rather than leaking a stack trace to the user. Libraries like Symfony's HttpKernel and Laravel's exception handler are built entirely around catching Throwable, not just Exception, for this reason.

try { doSomething(); } catch (Exception $e) { // handle it } try { doSomething(); } catch (Throwable $e) { // handle it -- also catches TypeError, DivisionByZeroError, etc. } catch (Exception $e) will not catch Error or any of its subclasses (TypeError, DivisionByZeroError, ArgumentCountError...) because Error and Exception are siblings under Throwable, not parent/child. If a function you call can fail with either kind of Throwable and you only catch Exception, an Error will propagate up uncaught.

[multiple-choice] Which statement about Error and Exception is correct? A. Error extends Exception B. Exception extends Error C. Error and Exception are separate classes that both implement Throwable, with neither extending the other D. Error and Exception are the same class with two names

C. Error and Exception are separate classes that both implement Throwable, with neither extending the other — Throwable is the common interface; Error and Exception are siblings that both implement it directly. This is why catch (Exception $e) will not catch a TypeError, but catch (Throwable $e) catches both branches.

[true-false] A parse error in your application's top-level entry script can always be caught with a try/catch block. A. True B. False

B. False — False. A parse error means PHP could not even compile the script, so no code of yours -- including a try/catch -- ever begins executing. A ParseError is only catchable when the malformed file is include()'d/require()'d from within another script that is already running.

Write a function safeDivide(int $a, int $b): int|string that returns the integer division result of $a / $b, but if $b is zero, catches the resulting engine error and returns the string 'Cannot divide by zero' instead of letting the script crash.

function safeDivide(int $a, int $b): int|string { try { return intdiv($a, $b); } catch (DivisionByZeroError $e) { return 'Cannot divide by zero'; } }

Integer division by zero throws DivisionByZeroError, a subclass of Error -- use intdiv() and a try/catch around it.