In PHP 8, what is the difference between the Error and Exception classes, and why does catch (Exception $e) miss a TypeError?
answer
- one interface, two branches
- Throwable is the common root
- Error = fault the engine detects
- TypeError extends Error, not Exception
- classes cannot implement Throwable directly
basics
~20 sBoth 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 sPHP'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
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
Draw the tree: Throwable at the root, Error and Exception as separate branches, and know that catch (Exception) misses TypeError.
Name the main Error subclasses and which extension exceptions sit on the Exception side, and explain why a class cannot implement Throwable directly.
Audit legacy catch (Exception) safety nets after upgrades and decide where a Throwable boundary belongs in requests, jobs and commands.
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.