skip to content

In PHP 8, what is the difference between the Error and Exception classes, and why does catch (Exception $e) miss a TypeError?

level: juniorimportance: must knowfreq 72%

answer

  1. one interface, two branches
  2. Throwable is the common root
  3. Error = fault the engine detects
  4. TypeError extends Error, not Exception
  5. classes cannot implement Throwable directly

basics

~20 s

Both implement the Throwable interface but sit on separate branches: Error covers faults PHP itself detects, like TypeError or DivisionByZeroError, while Exception covers failures application and library code reports. catch (Exception) never matches an Error subclass.

solid answer

~40 s

PHP's throwable objects share one root, the `Throwable` interface, with two branches beneath it. `Error` is what the **engine** throws for faults it detects: `TypeError`, `ValueError`, `ArgumentCountError`, `DivisionByZeroError`, `ParseError`, and more. `Exception` is the base for failures **code decides to report**: SPL's `InvalidArgumentException` and `RuntimeException`, `JsonException`, `PDOException`, and your own classes. Neither extends the other, so `catch (Exception $e)` does not match a `TypeError`: it passes through and, if nothing catches it, ends the script as `Fatal error: Uncaught TypeError`. To catch both you name `Throwable` (or `Error` explicitly). You cannot write a class that implements `Throwable` directly; extend `Exception` or `Error`. As a rule, application code extends `Exception`, and `Error` subclasses signal bugs to fix.

code

php · 16 lines
php
<?php
declare(strict_types=1);

function label(string $s): string { return strtoupper($s); }

try {
    echo label(42);                   // TypeError under strict_types
} catch (Exception $e) {
    echo 'never reached', PHP_EOL;    // TypeError is not an Exception
} catch (Throwable $t) {
    echo get_class($t), ': ', $t->getMessage(), PHP_EOL;
}
// TypeError: label(): Argument #1 ($s) must be of type string, int given, called in ...

var_dump(new TypeError() instanceof Exception);  // bool(false)
var_dump(new TypeError() instanceof Throwable);  // bool(true)

go deeper

for a junior

Draw the tree: Throwable at the root, Error and Exception as separate branches, and know that catch (Exception) misses TypeError.

for a middle

Name the main Error subclasses and which extension exceptions sit on the Exception side, and explain why a class cannot implement Throwable directly.

for a senior

Audit legacy catch (Exception) safety nets after upgrades and decide where a Throwable boundary belongs in requests, jobs and commands.

for a principal

Set a codebase convention for which branch custom failures extend and how boundaries log Errors as bugs rather than handle them as conditions.

## One root, two branches In PHP 8 every object you can `throw` implements the **`Throwable`** interface. Beneath it are two separate class trees: ``` Throwable ├── Error │ ├── TypeError │ │ └── ArgumentCountError │ ├── ValueError │ ├── ArithmeticError │ │ └── DivisionByZeroError │ ├── CompileError │ │ └── ParseError │ ├── AssertionError │ └── UnhandledMatchError └── Exception ├── ErrorException ├── LogicException (SPL) ├── RuntimeException (SPL) │ ├── PDOException │ └── mysqli_sql_exception └── JsonException ``` The tree was introduced with PHP 7, which moved most fatal engine errors onto the `Error` branch so they could be caught. PHP 8.0 then turned many former warnings into `Error` subclasses as well. ## What each branch means - **`Error`** — thrown by the engine or a built-in function when code is used incorrectly: a wrong argument type, an impossible value, division by zero, calling an undefined function, a syntax error in an included file. These usually point to a **bug** in the calling code. - **`Exception`** — the base for failures that code deliberately reports: a record not found, a network timeout, invalid JSON when `JSON_THROW_ON_ERROR` is used, a database error from PDO. These are often **expected conditions** the program handles. Extensions pick a side deliberately. `JsonException` and `PDOException` are exceptions because bad input or a lost connection is a normal runtime condition; `ValueError` is an error because passing `-1` where a count is required is a programming mistake. ## Why catch (Exception) misses a TypeError A `catch` clause matches by class: `catch (Exception $e)` matches `Exception` and its subclasses only. `TypeError` extends `Error`, and `Error` does not extend `Exception`, so the clause simply does not apply. The `TypeError` keeps propagating; if no outer handler catches it, PHP prints `Fatal error: Uncaught TypeError: ...` and stops the script. This is the most common surprise after upgrading legacy code: a PHP 5-era `try { ... } catch (Exception $e) { log(); }` around a library call looked like a safety net, but the failures PHP 8 throws for misuse are on the other branch. | Catch clause | Catches `RuntimeException` | Catches `TypeError` | |---|---|---| | `catch (Exception $e)` | yes | no | | `catch (Error $e)` | no | yes | | `catch (Throwable $e)` | yes | yes | ## The Throwable interface itself `Throwable` extends `Stringable` and declares the methods both branches share: `getMessage()`, `getCode()`, `getFile()`, `getLine()`, `getTrace()`, `getTraceAsString()` and `getPrevious()`. You may **type-hint** it, **catch** it, and write **interfaces** that extend it — `interface PaymentFailure extends Throwable` is a common marker pattern. What you cannot do is implement it in a class that extends neither base: `class AppFailure implements Throwable {}` is a fatal error, `cannot implement interface Throwable, extend Exception or Error instead`. ## Checking the branch at run time When a boundary catches `Throwable`, it often needs to know which branch it received: - `$t instanceof Error` separates engine-detected faults from reported exceptions; - `$t::class` (or `get_class($t)`) gives the exact class for logs and metrics; - `$t->getPrevious()` walks a chain of wrapped throwables, which may mix both branches — an `Exception` can wrap a `TypeError` and vice versa. Logging the class name alongside the message is what lets an on-call engineer tell a `TypeError` bug from a `PDOException` outage at a glance. ## What is not a Throwable Not every problem arrives as an object. Warnings, notices and deprecations are reported through the error-reporting system and do not interrupt execution. Some fatal conditions, such as exhausting `memory_limit`, are not thrown at all and cannot be caught with any `catch`. ## Practical rules 1. Extend `Exception` (usually an SPL subclass) for your own failures. 2. Catch the **narrowest** class you can actually handle. 3. Catch `Throwable` only at boundaries — the top of a request, a job runner, a CLI command — to log and fail cleanly. 4. Treat a caught `Error` as a bug report, not a condition to retry.

  • Can you define your own class that implements Throwable in PHP?
    Not directly: a class implementing `Throwable` must extend `Exception` or `Error`, otherwise PHP stops with `cannot implement interface Throwable, extend Exception or Error instead`. You can declare an interface that extends `Throwable` and implement it in an exception class, which lets callers catch a family by interface.
  • Should application code throw Error subclasses such as ValueError itself?
    It can, since they are ordinary classes, but most codebases reserve the `Error` branch for engine-detected misuse and throw `Exception` subclasses such as `InvalidArgumentException`. Throwing `Error` means callers using `catch (Exception)` will not see it, which surprises consumers of a library.

A building has two alarm systems: one for incidents staff report, one wired to the structure that trips on its own. A guard who only watches the staff-report panel never sees the structural alarm, however loud it is.

saying these in an interview costs you the question

  • TypeError is a subclass of Exception.
  • catch (Exception $e) catches everything PHP can throw.
  • Any class can implement Throwable directly.
  • Errors in PHP 8 cannot be caught at all.
  • Exception extends Error, so catching Error covers both.