Skip to content

7. Exceptions and Errors

When a method of a class encounters an unrecoverable error (file does not exist, database not connected, network connection down), it does not display an error on a console (file, database) but throws an exception. Exceptions extend the [\Exception] class. In addition to exceptions, the internal operation of PHP also throws errors whose base class is [\Error]. Both classes implement the [\Throwable] interface.

PHP 8 has fundamentally changed error handling: many situations that produced a simple warning (and often an absurd result) in PHP 7 now throw an error that can be caught. We’ll see this as we go through the examples.

7.1. The Script Directory Structure

The scripts in this chapter are located in the [scripts-console/exceptions] folder:

1
2
3
4
5
6
php8-oct-2026/scripts-console/exceptions
├── exceptions-01.php
├── exceptions-02.php
├── ...
├── exceptions-11.php
└── throwables.php

7.2. The [\Throwable] Interface

The [\Throwable] interface is as follows (see documentation):

interface Throwable extends Stringable
{
    public function getMessage(): string;
    public function getCode(): int;
    public function getFile(): string;
    public function getLine(): int;
    public function getTrace(): array;
    public function getTraceAsString(): string;
    public function getPrevious(): ?Throwable;
    public function __toString(): string;
}

The roles of the interface methods are as follows:

Method
Purpose
[getMessage]
Returns the error message
[getCode]
returns the error code (an integer)
[getFile]
returns the name of the file where the object was created
[getLine]
returns the line number where the object was created
[getTrace]
returns the function call stack as an array
[getTraceAsString]
returns the call stack as a string
[getPrevious]
returns the previous (encapsulated) object [Throwable], or [null]
[__toString]
returns a string representation of the object

Note that the [Throwable] interface extends the [Stringable] interface discussed in the chapter on classes. A class cannot directly implement the [Throwable] interface; it must derive from [\Exception] or [\Error].

7.3. Predefined exceptions and errors in PHP 8

The following [throwables.php] script displays the tree structure of the predefined classes that implement the [Throwable] interface. We use it to obtain an exact list—that of PHP 8.5—rather than copying it from documentation. The reader will not miss anything if they do not try to understand its code, which uses PHP's reflection API (the [ReflectionClass] class allows you to query PHP about a class: its name, its original extension, its interfaces, etc.) and a recursive function:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// displays the tree of the predefined classes implementing the [Throwable] interface
// we restrict ourselves to the extensions always present in PHP 8.5:
// the core (Core), the standard library, the SPL library, jSON and Random
$extensions = ["Core", "standard", "SPL", "json", "random"];

// the predefined classes of these extensions that implement [Throwable]
$classes = array_filter(
    get_declared_classes(),
    function (string $class) use ($extensions): bool {
        $reflection = new ReflectionClass($class);
        return $reflection->isInternal()
            && $reflection->implementsInterface(Throwable::class)
            && in_array($reflection->getExtensionName(), $extensions, true);
    }
);
sort($classes);

// recursive display of the child classes of the class [$parent]
function display(?string $parent, array $classes, string $indentation): void
{
    // the direct children of [$parent]
    $children = array_values(array_filter(
        $classes,
        fn(string $class): bool => (get_parent_class($class) ?: null) === $parent
    ));
    foreach ($children as $i => $child) {
        $last = $i === count($children) - 1;
        print $indentation . ($last ? "└── " : "├── ") . $child . "\n";
        display($child, $classes, $indentation . ($last ? "    " : "│   "));
    }
}

print "Throwable\n";
display(null, $classes, "");

Results:

> php throwables.php
Throwable
├── Error
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── AssertionError
│   ├── CompileError
│   │   └── ParseError
│   ├── FiberError
│   ├── Random\RandomError
│   │   └── Random\BrokenRandomEngineError
│   ├── TypeError
│   │   └── ArgumentCountError
│   ├── UnhandledMatchError
│   └── ValueError
└── Exception
    ├── ClosedGeneratorException
    ├── ErrorException
    ├── JsonException
    ├── LogicException
    │   ├── BadFunctionCallException
    │   │   └── BadMethodCallException
    │   ├── DomainException
    │   ├── InvalidArgumentException
    │   ├── LengthException
    │   └── OutOfRangeException
    ├── Random\RandomException
    ├── RequestParseBodyException
    └── RuntimeException
        ├── OutOfBoundsException
        ├── OverflowException
        ├── RangeException
        ├── UnderflowException
        └── UnexpectedValueException

Comments

  • line 13: the function [get_declared_classes] returns a list of all classes known at this point, whether predefined or not;
  • Lines 16–18: Only predefined (internal) classes are retained—those that implement the [Throwable] interface and belong to extensions that are always present in PHP 8.5 (line 9). Other extensions add their own exceptions (for example, [PDOException] for database access, which we will encounter later);
  • lines 24–36: a recursive function that displays the direct child classes of class [$parent], then the children of each of those, and so on. The function [get_parent_class] returns [false] for a class with no parent: the operator [?:] then transforms [false] into [null];
  • line 29: an arrow function (PHP 7.4), a shorthand form of an anonymous function reduced to an expression;
  • line 39: we start with classes without a parent: [Error] and [Exception].

The results show two families:

  • the [Exception] family: these are the exceptions thrown by libraries and application code. It includes:
    • [ErrorException], which converts a warning from PHP into an exception (we’ll use this at the end of the chapter);
    • [JsonException] (PHP 7.3), thrown by [json_encode] and [json_decode] with the [JSON_THROW_ON_ERROR] option;
    • [Random\RandomException] (PHP 8.2), initiated by the random number generation functions;
    • exceptions from the SPL library (Standard PHP Library), a collection of classes and interfaces designed to solve problems frequently encountered by developers. They form two subfamilies:
    • [LogicException] and its derivatives: programming errors that can be resolved by correcting the code: invalid argument ([InvalidArgumentException]), value outside the expected range ([DomainException]), nonexistent method ([BadMethodCallException])...;
    • [RuntimeException] and its variants: errors that can only be detected at runtime: index out of bounds ([OutOfBoundsException]), unexpected value ([UnexpectedValueException]), capacity exceeded ([OverflowException])... ;
  • the [Error] family: these are errors thrown by the PHP engine itself. They include:
    • [TypeError]: a value is not of the expected type (function parameter, typed attribute, return value, operand of an operator, etc.). Its derivative, [ArgumentCountError], indicates an insufficient number of arguments;
    • [ValueError] (PHP 8.0): a value is of the correct type but has an invalid value (for example, a negative or zero length);
    • [ArithmeticError] and its derivative [DivisionByZeroError]: errors in arithmetic operations;
    • [UnhandledMatchError] (PHP 8.0): No branch of the expression [match] is suitable;
    • [AssertionError]: [assert] assertion failed;
    • [CompileError] and its derivative [ParseError]: syntax error in a file loaded by [require] or [include];
    • [FiberError] (PHP 8.1) and [Random\RandomError] (PHP 8.2): errors related to fibers and random number generators, which we will not use.

The two families are unrelated: a [catch (\Exception $e)] clause does not intercept a [Error] error. Only the [Throwable] interface links them.

7.4. Example 1

The first example, [exceptions-01.php], shows both a PHP warning and an error:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// display of all errors
error_reporting(E_ALL);
ini_set("display_errors", "1");
// code --------
$var = [];
// unknown key
print $var["abcd"];
// division by zero
$var = 7 / 0;
var_dump($var);
// fixed-bounds array
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// index out of bounds
$array[5] = 8;

Comments

  • Line 7: PHP is instructed to report all errors. The parameter for the [error_reporting] function is the error reporting level. It is a combination of the following constants (the main ones):
Constant
Meaning
[E_ERROR]
Fatal runtime error: the script is terminated
[E_WARNING]
Warning: the script continues
[E_PARSE]
Syntax error detected during compilation
[E_NOTICE]
Note: Something that might be an error
[E_DEPRECATED]
Use of a deprecated feature, which will be removed in a future version
[E_USER_WARNING], [E_USER_NOTICE], [E_USER_DEPRECATED]
The same levels, triggered by the code using the [trigger_error] function
[E_ALL]
all levels
  • the constant [E_STRICT], which no longer corresponds to any message since PHP 8.0, has been deprecated since PHP 8.4, as is the use of level [E_USER_ERROR] with [trigger_error]: to signal a fatal error, the code throws an exception;
  • line 8: the code is set to display errors on the console. In production, the opposite is done: errors are written to a log file and not displayed;
  • line 12: an attempt is made to access a nonexistent element of the array [$var];
  • line 14: a division by zero occurs;
  • line 17: an instance of the [SplFixedArray] class from the SPL library is created. This class allows you to create a fixed-bound array with integer indices;
  • line 21: an attempt is made to access a non-existent element of the array.

Results:

1
2
3
4
5
6
7
8
> php exceptions-01.php

Warning: Undefined array key "abcd" in .../php8-oct-2026/scripts-console/exceptions/exceptions-01.php on line 12

Fatal error: Uncaught DivisionByZeroError: Division by zero in .../php8-oct-2026/scripts-console/exceptions/exceptions-01.php:14
Stack trace:
#0 {main}
  thrown in .../php8-oct-2026/scripts-console/exceptions/exceptions-01.php on line 14

Comments on the results

  • line 3: Accessing a nonexistent key in an array triggers a warning of level [E_WARNING] (this was a simple note [E_NOTICE] in PHP 7). This does not interrupt the script’s execution;
  • Lines 5–8: In PHP 7, dividing a number by zero triggered a [E_WARNING] warning and returned the value [INF] (infinity). In PHP 8, the operator [/] triggers an error of type [DivisionByZeroError]. Since it is not caught, it interrupts the script’s execution: this is a fatal error (Fatal error: Uncaught...). The message displays the call stack (Stack trace) at the time of the error;
  • the code following the division is not executed: the attempt to access a non-existent index in the array [SplFixedArray] does not occur. We’ll see this again in the following examples.

7.5. Handling Exceptions

The script [exceptions-02.php] demonstrates how to handle exceptions:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// we display all errors
error_reporting(E_ALL);
ini_set("display_errors", "1");
// we surround the code with a try / catch
try {
    $var = [];
    // unknown key
    print $var["abcd"];
    // division by zero
    $var = 7 / 0;
    var_dump($var);
    // fixed-bounds array
    $array = new \SplFixedArray(5);
    $array[1] = 2;
    $array[4] = "foo";
    // index out of bounds
    $array[5] = 8;
    // check
    print "this message will not be displayed\n";
} catch (\Throwable $ex) {
    // \Throwable is the interface implemented by all errors and exceptions
    // we display the exception
    print "error, message: " . $ex->getMessage() . ", type: " . $ex::class . "\n";
}

Comments

  • The script is the one presented in the previous paragraph. However, we have now enclosed the code that could cause errors within a [try / catch] structure: if the code in lines 11–24 causes (throws) an exception or an error, it will be handled by the [catch] clause in lines 25–29;
  • Line 25: The parameter of the [catch] clause is the type of exception or error to be handled. By setting the type to [\Throwable], which is an interface, we indicate that we want to handle any class instance that implements the [\Throwable] interface. Since all error and exception classes implement this interface, the [catch] clause handles all errors and exceptions here;
  • line 15: the statement that triggers the error. As soon as an exception occurs, control is transferred to the [catch] clause. The code that follows in [try] is therefore not executed;
  • line 28: the notation [$ex::class] returns the class name of the object [$ex].

Results:

1
2
3
4
> php exceptions-02.php

Warning: Undefined array key "abcd" in .../php8-oct-2026/scripts-console/exceptions/exceptions-02.php on line 13
error, message: Division by zero, type: DivisionByZeroError

Comments on the results

  • Line 3: We see the warning at level [E_WARNING]. A warning is not an exception and is therefore not handled by the clause [catch]. We will see at the end of this chapter how to convert warnings into exceptions;
  • Line 4: The error message is written in the [catch] clause. This means an exception derived from [\Exception] or an error derived from [\Error] occurred. Here we see that this is the class [DivisionByZeroError]. The script completed normally.

7.6. Parameters of the [catch] clause

Let’s examine the following [exceptions-03.php] script:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// we display all errors
error_reporting(E_ALL);
ini_set("display_errors", "1");

// a fixed-bounds array
$array = new \SplFixedArray(5);
try {
    // index out of bounds
    $array[5] = 8;
} catch (\Throwable $ex) {
    // display of the error message
    print "Error 1: " . $ex->getMessage() . " (" . $ex::class . ")\n";
}

try {
    // index out of bounds
    $array[5] = 8;
} catch (\Exception $ex) {
    // display of the error message
    print "Error 2: " . $ex->getMessage() . "\n";
}

try {
    // index out of bounds
    $array[5] = 8;
} catch (\RuntimeException $ex) {
    // display of the error message
    print "Error 3: " . $ex->getMessage() . "\n";
}

try {
    // index out of bounds
    $array[5] = 8;
} catch (\OutOfBoundsException $ex) {
    // display of the error message
    print "Error 4: " . $ex->getMessage() . "\n";
}

try {
    // division by 0
    intdiv(5, 0);
} catch (\Throwable $ex) {
    // display of the error message
    print "Error 5: " . $ex->getMessage() . " (" . $ex::class . ")\n";
}

try {
    // division by 0
    intdiv(5, 0);
} catch (\DivisionByZeroError $ex) {
    // display of the error message
    print "Error 6: " . $ex->getMessage() . "\n";
}

try {
    // division by 0
    intdiv(5, 0);
} catch (\ArithmeticError $ex) {
    // display of the error message
    print "Error 7: " . $ex->getMessage() . "\n";
}

try {
    // division by 0
    intdiv(5, 0);
} catch (\Error $ex) {
    // display of the error message
    print "Error 8: " . $ex->getMessage() . "\n";
}

try {
    // division by 0
    intdiv(5, 0);
} catch (\Exception $ex) {
    // display of the error message
    print "Error 9: " . $ex->getMessage() . "\n";
}

Comments

  • Lines 11–41: Four different ways to handle the exception generated by using an incorrect index with class [\SplFixedArray]. The execution shows that this error generates an exception of type [OutOfBoundsException] (line 2 of the results). In the PHP 7 course (and again in PHP 8.3), it was a [RuntimeException] exception. The change did not break the existing code since the [OutOfBoundsException] class derives from the [RuntimeException] class;
    • line 15: handles an error of type [\Throwable]. This is valid because the type [OutOfBoundsException] derives from the type [\Exception], which implements the interface [\Throwable];
    • line 23: handles an error of type [\Exception]. This is valid because the type [OutOfBoundsException] derives from the type [\Exception];
    • line 31: handles an error of type [\RuntimeException]. This is valid because the type [OutOfBoundsException] derives from the type [\RuntimeException]. This is a good choice: the code works regardless of the version of PHP;
    • line 39: handles the exact type of the exception generated;
  • lines 46–81: 5 different ways to handle the error generated by the function [intdiv] when a divisor equal to 0 is passed to it. The function [intdiv(int $dividend, int $divisor): int] performs the integer division [$dividend / $divisor]. When the divisor is zero, error [\DivisionByZeroError] is thrown;
    • line 47: any error implementing the [\Throwable] interface is caught. This is valid;
    • line 55: the exact type of the error is intercepted; this is the preferred method;
    • line 63: the type [\ArithmeticError] is caught. This is valid since the class [DivisionByZeroError] extends the class [ArithmeticError];
    • Line 71: The type [\Error] is intercepted. This is valid because the class [DivisionByZeroError] derives from the class [Error];
    • Line 79: The type [\Exception] is intercepted. This is invalid because the class [DivisionByZeroError] has no relationship with the class [\Exception].

Results:

> php exceptions-03.php
Error 1: Index invalid or out of range (OutOfBoundsException)
Error 2: Index invalid or out of range
Error 3: Index invalid or out of range
Error 4: Index invalid or out of range
Error 5: Division by zero (DivisionByZeroError)
Error 6: Division by zero
Error 7: Division by zero
Error 8: Division by zero

Fatal error: Uncaught DivisionByZeroError: Division by zero in .../php8-oct-2026/scripts-console/exceptions/exceptions-03.php:78
Stack trace:
#0 .../php8-oct-2026/scripts-console/exceptions/exceptions-03.php(78): intdiv(5, 0)
#1 {main}
  thrown in .../php8-oct-2026/scripts-console/exceptions/exceptions-03.php on line 78

Lines 11–15 of the results show that the error [DivisionByZeroError] was not caught by the last clause [catch (\Exception $ex)]: this is a fatal error.

7.7. Clause [finally]

The structure [try / catch] may have a third element and become a structure [try / catch / finally]. The code in clause [finally] is executed in the following two cases:

  • the [try] clause does not throw an exception. It is then executed in its entirety, after which execution proceeds to the [finally] clause, which is executed in its entirety;
  • The clause [try] throws an exception. It is then executed up to the statement that throws the exception. Execution then proceeds to clause [catch], which is executed in its entirety. Execution then proceeds to clause [finally], which is executed in its entirety.

Finally, the code in clause [finally] is still executed. This scenario is useful in the following case:

  • In the [try] clause, the code has acquired resources (files, databases, network connections, queues). These resources are generally expensive. They must therefore be released (most often referred to as “closed”) as soon as possible;
  • if the resources were acquired in [try], their release will be handled in [finally]. This ensures that in all cases (whether an error occurs or not), the acquired resources are returned to the system.

The following script, [exceptions-04.php], demonstrates how the [finally] clause works in various situations:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// we create an exception instance
$e = new \Exception("Error...");

// first test
try {
    print "First test\n";
    throw $e;
} catch (\Exception $ex1) {
    print $ex1->getMessage() . "\n";
} finally {
    print "Done\n";
}

// second test
try {
    print "Second test\n";
} catch (\Exception $ex1) {
    print $ex1->getMessage() . "\n";
} finally {
    print "Done\n";
}

// third test
try {
    print "Third test\n";
    return;
} catch (\Exception $ex1) {
    print $ex1->getMessage() . "\n";
} finally {
    print "Done\n";
}

Code Comments

  • Line 7: [$e] is an instance of the predefined class [\Exception]. We will instantiate it in different places;
  • Lines 11–12: The exception [$e] is thrown in [try] by the statement [throw];
  • line 13: the [\Exception] exception is caught, and its error message is written to the console;
  • Lines 15–16: The [finally] clause writes a message. Based on what was mentioned earlier, this message should always be written, regardless of whether there is an error in [try];
  • line 21: there is no error in [try]. Here, too, the program should proceed to [finally];
  • Line 31: There is a [return] statement in the [try] clause, and no error. This raises the question of whether the script will proceed to the [finally] clause. Execution shows that it does. When used outside a function, the [return] statement terminates the script.

Results:

1
2
3
4
5
6
7
8
> php exceptions-04.php
First test
Error...
Done
Second test
Done
Third test
Done

Let’s examine another case, [exceptions-05.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// fourth test
try {
    print "Fourth test\n";
    exit;
} finally {
    print "Done\n";
}

Comments

  • Line 9: The statement [exit] immediately terminates script execution: the clause [finally] is not executed;
  • Lines 7–12: An example of [try / finally] without the [catch] clause. This is possible.

Results:

> php exceptions-05.php
Fourth test

7.8. Creating Your Own Exception Classes

In a somewhat large project, it’s useful to differentiate between various errors by encapsulating them in different exception classes. In the previous scripts, we saw that any exception could be caught by a [catch (\Throwable $ex)] clause. This is recommended if you have no idea what the intercepted error is and the handling is the same for all errors. This is sometimes the case, but you often need to adapt the handling to the exact type of error. In that case, you need to distinguish between the different errors.

Let’s examine the following [exceptions-06.php] script:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// we define our own family of exceptions
class Exception1 extends \RuntimeException
{
}

class Exception2 extends \RuntimeException
{
}

// we use our exceptions
$e1 = new Exception1("Error1...");
var_dump($e1);
$e2 = new Exception2("Error2...", 2);

// first test
print "first test\n";
try {
    // we throw an Exception1 type
    throw $e1;
} catch (Exception1 $ex1) {
    print "Exception 1\n";
    print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
    print "Exception 2\n";
    print $ex2->getMessage() . "\n";
}

// second test
print "second test\n";
try {
    // we throw an Exception2 type
    throw $e2;
} catch (Exception1 $ex1) {
    print "Exception 1\n";
    print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
    print "Exception 2\n";
    print $ex2->getMessage() . "\n";
}

// third test
print "third test\n";
try {
    // we throw an Exception1 type
    throw $e1;
} catch (Exception1 | Exception2 $ex) {
    print "Exception 1 or 2\n";
    print $ex->getMessage() . "\n";
}

// fourth test
print "fourth test\n";
try {
    // we throw an Exception2 type
    throw $e2;
} catch (Exception1 | Exception2 $ex) {
    print "Exception 1 or 2: " . $ex::class . "\n";
    // the information carried by the exception
    print "message: " . $ex->getMessage() . "\n";
    print "code: " . $ex->getCode() . "\n";
    print "file: " . basename($ex->getFile()) . "\n";
    print "line: " . $ex->getLine() . "\n";
}

Comments

  • Lines 7–9: We define two classes, *[Exception1] and *[Exception2], both derived from the predefined class ***[\\RuntimeException]***. The body of these classes is empty. In other words, they are used solely for their types: it is because they have different types that we will be able to distinguish between these two exceptions in the [catch] clauses;
  • lines 16–18: We define two variables, [$e1] and [$e2], which have the types [Exception1] and [Exception2], respectively. The constructor used is that of the [Exception] class, whose signature is [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. For [$e2], an error code has been specified;
  • lines 21–31: there is a [try / catch / catch] structure. This allows for handling different types of exceptions with different [catch] clauses. The first clause whose type matches is executed; the others are ignored;
  • line 25: intercepts exceptions of type [Exception1];
  • line 28: intercepts exceptions of type [Exception2];
  • line 51: intercepts exceptions of type [Exception1] or ([|]) [Exception2]. This syntax has been available since PHP 7.1;
  • line 62: the class name of the intercepted exception;
  • lines 64–67: the information contained in the exception, obtained via the methods of the [Throwable] interface. The [basename] function returns a filename without the folder path.

Results:

> php exceptions-06.php
object(Exception1)#1 (7) {
  ["message":protected]=>
  string(9) "Error1..."
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(76) ".../php8-oct-2026/scripts-console/exceptions/exceptions-06.php"
  ["line":protected]=>
  int(16)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  NULL
}
first test
Exception 1
Error1...
second test
Exception 2
Error2...
third test
Exception 1 or 2
Error1...
fourth test
Exception 1 or 2: Exception2
message: Error2...
code: 2
file: exceptions-06.php
line: 18

Comments on the results

  • Lines 2–18: The “content” of an exception, displayed by [var_dump]:
    • lines 3–4: the error message;
    • lines 7–8: the error code;
    • lines 9–10: the name of the file in which the exception was raised. The length indicated (string(76)) is that of the full file path: it depends on the folder in which the script is located;
    • lines 11–12: the line where the exception was raised;
    • lines 13–15: the call stack at the time the exception was raised. It is empty here because the exception was raised in the main script and not within a function;
    • lines 16–17: the previous exception. An exception can encapsulate another exception, thereby defining a stack of exceptions. The [previous] attribute allows this stack to be analyzed;
  • lines 29–33: Note that the file and line number of the exception are those from its creation by [new] (line 18) and not those from its throw by [throw].

7.9. Re-throwing an Exception

An exception can be thrown multiple times, as shown in the following script, [exceptions-07.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

try {
    try {
        // we throw an exception
        throw new \Exception("test");
    } catch (\Exception $ex) {
        // we rethrow the caught exception
        throw $ex;
    } finally {
        // we do go through the finally
        print "finally 1\n";
    }
} catch (\Exception $ex2) {
    // we do get back the initial exception
    print $ex2->getMessage() . " in outer try / catch / finally\n";
} finally {
    // we do go through the finally
    print "finally 2\n";
}

Comments

  • line 9: an exception is thrown;
  • line 10: the exception is caught;
  • line 12: the exception is rethrown. It is then passed to the [try / catch / finally] structure at the higher level. This scenario is common: code intercepts an exception to perform local processing (write a log, roll back a transaction, etc.) and then rethrows it so that the calling code is also notified;
  • line 17: it is intercepted again;
  • Lines 13–15: The execution shows that even after the [throw] in line 12, the program does indeed proceed to the [finally] clause of the internal [try / catch / finally].

Results:

1
2
3
4
> php exceptions-07.php
finally 1
test in outer try / catch / finally
finally 2

7.10. Handling an Exception Stack

An exception can encapsulate another exception, which in turn can encapsulate yet another, ultimately forming an exception stack. Here is an example: [exceptions-08.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// we define our own family of exceptions
class Exception1 extends \RuntimeException
{
}

class Exception2 extends \RuntimeException
{
}

class Exception3 extends \RuntimeException
{
}

// we use our exceptions
$e1 = new Exception1("Error 1...", 1, new Exception2("Error 2...", 2, new Exception3("Error 3...")));
var_dump($e1);
// using the stack of exceptions
$e = $e1;
do {
    // error message of the current exception
    print $e::class . ": " . $e->getMessage() . "\n";
    // we move on to the previous exception (null if there is none)
    $e = $e->getPrevious();
} while ($e !== null);

Comments

  • Lines 7–15: Define three exception classes derived from the predefined exception [RuntimeException];
  • line 20: an instance of the [Exception3] class is encapsulated within an instance of the [Exception2] class, which is itself encapsulated within an instance of the [Exception1] class. The constructor used here is the constructor for the [Exception] class: [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. The third parameter of the constructor allows an exception to be encapsulated. This can be useful in the following scenario:
    • a method M is defined that can throw an exception of type [Exception1]—and only that type—for compatibility reasons, such as with an interface;
    • however, other types of exceptions may occur within method M. To propagate an error back to the code calling method M, these exceptions are then encapsulated into the type [Exception1], which is then thrown. This ensures that the information contained in the encapsulated exception—which was the original cause of the error—is not lost;
  • Lines 24–29: demonstrate how to traverse the stack of exceptions nested within an exception. The method [getPrevious] returns the encapsulated exception or [null] if there is none. The [do ... while] loop executes its body at least once and then continues as long as the condition is true.

Results:

> php exceptions-08.php
object(Exception1)#1 (7) {
  ["message":protected]=>
  string(10) "Error 1..."
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(1)
  ["file":protected]=>
  string(76) ".../php8-oct-2026/scripts-console/exceptions/exceptions-08.php"
  ["line":protected]=>
  int(20)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  object(Exception2)#2 (7) {
    ["message":protected]=>
    string(10) "Error 2..."
    ["string":"Exception":private]=>
    string(0) ""
    ["code":protected]=>
    int(2)
    ["file":protected]=>
    string(76) ".../php8-oct-2026/scripts-console/exceptions/exceptions-08.php"
    ["line":protected]=>
    int(20)
    ["trace":"Exception":private]=>
    array(0) {
    }
    ["previous":"Exception":private]=>
    object(Exception3)#3 (7) {
      ["message":protected]=>
      string(10) "Error 3..."
      ["string":"Exception":private]=>
      string(0) ""
      ["code":protected]=>
      int(0)
      ["file":protected]=>
      string(76) ".../php8-oct-2026/scripts-console/exceptions/exceptions-08.php"
      ["line":protected]=>
      int(20)
      ["trace":"Exception":private]=>
      array(0) {
      }
      ["previous":"Exception":private]=>
      NULL
    }
  }
}
Exception1: Error 1...
Exception2: Error 2...
Exception3: Error 3...
  • lines 2–50: the exception [$e1] contains, in its attribute [previous], the exception [Exception2] (lines 17–49), which itself contains the exception [Exception3] (lines 32–48);
  • lines 51–53: the exception stack trace.

7.11. Warnings from PHP 7 that became errors in PHP 8

We have seen that division by zero, which produced a warning in PHP 7, triggers an error in PHP 8. This is not an isolated case: PHP 8 has converted many warnings—which previously continued execution with a false result—into errors. The script [exceptions-09.php] illustrates a few of them:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// displays the type and the message of an error
function display(int $n, \Throwable $ex): void
{
    print "$n - " . $ex::class . ": " . $ex->getMessage() . "\n";
}

// a function with two parameters
function sum(int $a, int $b): int
{
    return $a + $b;
}

// 1 - undefined variable: a mere warning (Notice in PHP 7, Warning in PHP 8)
print "1 - [" . $unknown . "]\n";
// 2 - modulo by zero: warning + wrong result in PHP 7, error in PHP 8
try {
    print 1 % 0;
} catch (\DivisionByZeroError $ex) {
    display(2, $ex);
}
// 3 - arithmetic operation on an array
try {
    print [] + 1;
} catch (\TypeError $ex) {
    display(3, $ex);
}
// 4 - arithmetic operation on a non-numeric string
try {
    print "abc" * 2;
} catch (\TypeError $ex) {
    display(4, $ex);
}
// 5 - wrong parameter type for a predefined function
try {
    print strlen([1, 2]);
} catch (\TypeError $ex) {
    display(5, $ex);
}
// 6 - parameter of the right type but with an invalid value
try {
    print_r(array_chunk([1, 2, 3], 0));
} catch (\ValueError $ex) {
    display(6, $ex);
}
// 7 - not enough arguments
try {
    print sum(1);
} catch (\ArgumentCountError $ex) {
    display(7, $ex);
}
// 8 - [match] expression without a matching arm (PHP 8.0)
try {
    $day = 8;
    print match ($day) {
        6, 7 => "weekend",
        1, 2, 3, 4, 5 => "weekday",
    };
} catch (\UnhandledMatchError $ex) {
    display(8, $ex);
}
// 9 - method called on null
try {
    $object = null;
    $object->method();
} catch (\Error $ex) {
    display(9, $ex);
}
// 10 - the nullsafe operator ?-> avoids the previous error: the result is null
var_dump($object?->method());

Results:

> php exceptions-09.php

Warning: Undefined variable $unknown in .../php8-oct-2026/scripts-console/exceptions/exceptions-09.php on line 19
1 - []
2 - DivisionByZeroError: Modulo by zero
3 - TypeError: Unsupported operand types: array + int
4 - TypeError: Unsupported operand types: string * int
5 - TypeError: strlen(): Argument #1 ($string) must be of type string, array given
6 - ValueError: array_chunk(): Argument #2 ($length) must be greater than 0
7 - ArgumentCountError: Too few arguments to function sum(), 1 passed in .../php8-oct-2026/scripts-console/exceptions/exceptions-09.php on line 52 and exactly 2 expected
8 - UnhandledMatchError: Unhandled match case 8
9 - Error: Call to a member function method() on null
NULL

Comments

  • Line 19: The use of an undefined variable remains a simple warning, but at the [E_WARNING] level (it was a [E_NOTICE] note in PHP 7). The variable is set to [null], and the script continues (lines 2–4 of the results);
  • line 22: division by zero triggers an error [DivisionByZeroError] (line 5). This was already the case in PHP 7: only division by zero in [/] produced a simple warning there;
  • line 28: an arithmetic operation on an array causes a [TypeError] error (line 6);
  • line 34: in PHP 7, an arithmetic operation on a non-numeric string triggered the warning “A non-numeric value encountered,” and the string was 0. In PHP 8, this is an error [TypeError] (line 7). A numeric string (["12" * 2]) is still accepted;
  • line 40: In PHP 7, passing a parameter of the wrong type to a predefined function triggered a warning, and the function returned [null]. In PHP 8, all predefined functions throw an error [TypeError], just like functions written by the developer (line 8). The message identifies the parameter by its number and name;
  • line 46: the parameter has the correct type ([int]) but an invalid value (a length must be positive): this is error [ValueError], new in PHP 8.0 (line 9). In PHP 7, this was a warning;
  • line 52: the [sum] function expects two arguments but receives only one: error [ArgumentCountError] (line 10);
  • Lines 59–62: The expression [match] has no branch for the value 8 and has no branch [default]: error [UnhandledMatchError] (line 11). This is a significant difference from the [switch] statement, which does nothing when no [case] is suitable;
  • line 69: Calling a method on [null] results in an error [Error] (line 12). This was already the case in PHP 7;
  • Line 74: The null-safe operator [?->] (PHP 8.0) returns [null] if the object is [null], instead of calling the method. This avoids having to write a test for [if ($object !== null)] (line 13).

These changes make PHP 8 programs safer: an error halts processing instead of allowing it to continue with incorrect data. They also require the developer to include the corresponding [catch] clauses.

7.12. Multiple [catch], [catch] without a variable, and [throw] expression

The [exceptions-10.php] script offers three features of PHP 8 and a common use of exception handling:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// [throw] is an expression since PHP 8.0: it can be used after ?? or in an arrow function
function readAge(array $data): int
{
    $age = $data["age"] ?? throw new \InvalidArgumentException("The age is missing");
    return is_int($age) && $age >= 0 ? $age : throw new \DomainException("Invalid age [$age]");
}

// an arrow function that only throws an exception
$forbidden = fn(string $operation) => throw new \LogicException("Operation [$operation] forbidden");

// test data
$tests = [["age" => 48], [], ["age" => -4]];
foreach ($tests as $i => $data) {
    try {
        print "test $i: age=" . readAge($data) . "\n";
    } catch (\InvalidArgumentException | \DomainException $ex) {
        // a single [catch] clause for two types of exceptions
        print "test $i: " . $ex::class . ": " . $ex->getMessage() . "\n";
    }
}

try {
    $forbidden("deletion");
} catch (\LogicException) {
    // since PHP 8.0, the variable of the [catch] clause is optional
    print "the operation was refused\n";
}

// wrapping a low-level exception in an application exception
class DaoException extends \RuntimeException
{
}

function readConfiguration(string $json): array
{
    try {
        return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
    } catch (\JsonException $ex) {
        // the original cause is passed as 3rd parameter: it is not lost
        throw new DaoException("Unreadable configuration", 10, $ex);
    }
}

try {
    readConfiguration('{"dsn": "mysql:host=localhost"');
} catch (DaoException $ex) {
    print $ex::class . ": " . $ex->getMessage() . " (code " . $ex->getCode() . ")\n";
    print "cause: " . $ex->getPrevious()::class . ": " . $ex->getPrevious()->getMessage() . "\n";
}

Results:

1
2
3
4
5
6
7
> php exceptions-10.php
test 0: age=48
test 1: InvalidArgumentException: The age is missing
test 2: DomainException: Invalid age [-4]
the operation was refused
DaoException: Unreadable configuration (code 10)
cause: JsonException: Syntax error

Comments

  • Line 9: Starting with PHP 8.0, [throw] is an expression and no longer just an instruction. It can therefore be used wherever a value is expected: here, after the [??] operator. If the key [age] does not exist, an exception is thrown; otherwise, [$age] is assigned its value;
  • line 10: [throw] used in a conditional expression [? :];
  • line 14: an arrow function whose body simply throws an exception. Prior to PHP 8.0, this was not possible: an arrow function can contain only one expression;
  • line 21: a single clause [catch] for two types of exceptions (lines 3–4 of the results). Note that [InvalidArgumentException] and [DomainException] both derive from [LogicException]: a [catch (\LogicException $ex)] would have had the same effect;
  • line 29: Starting with PHP 8.0, the variable in the [catch] clause is optional when you do not need the exception itself, only its type (line 5);
  • lines 35–37: an application-specific exception. In the following chapters, we will write data access layers (DAO, Data Access Object) that will throw only this type of exception, regardless of the source of the error (file, JSON, database);
  • lines 39–47: The function [readConfiguration] decodes a JSON string. The low-level exception [JsonException] is intercepted and wrapped (line 45) in a [DaoException] exception. The original cause is passed as the third parameter to the constructor;
  • line 53: the calling code only knows [DaoException], but it can retrieve the original cause of the error using the [getPrevious] method (lines 6–7 of the results).

7.13. Convert warnings to exceptions: [ErrorException]

Despite the improvements in PHP 8, certain anomalies remain mere warnings: nonexistent array key, undefined variable, file cannot be opened with [fopen]... These warnings cannot be intercepted by a [try / catch]. However, you can install an error handler that converts them into [ErrorException] exceptions. This is what the [exceptions-11.php] script does:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// error handler: turns PHP warnings into [ErrorException] exceptions
set_error_handler(function (int $level, string $message, string $file, int $line): bool {
    throw new \ErrorException($message, 0, $level, $file, $line);
});

// an unknown key no longer causes a mere warning but an exception
$person = ["lastName" => "Langevin"];
try {
    print $person["firstName"] . "\n";
} catch (\ErrorException $ex) {
    print "Error: " . $ex->getMessage() . " (line " . $ex->getLine() . ")\n";
    print "level E_WARNING: " . var_export($ex->getSeverity() === E_WARNING, true) . "\n";
}
// same for opening a non-existent file
try {
    $stream = fopen(__DIR__ . "/nonexistent.txt", "r");
} catch (\ErrorException $ex) {
    print "Error: " . $ex->getMessage() . "\n";
}
// we go back to the PHP error handler
restore_error_handler();
print "end\n";

Results:

1
2
3
4
5
> php exceptions-11.php
Error: Undefined array key "firstName" (line 14)
level E_WARNING: true
Error: fopen(.../php8-oct-2026/scripts-console/exceptions/nonexistent.txt): Failed to open stream: No such file or directory
end

Comments

  • Lines 7–9: The [set_error_handler] function sets up an error handler—a function that PHP will call whenever a warning, note, or deprecation occurs, instead of displaying the message. It receives the error level, the error message, the file, and the line where the error occurred. Here, it throws a [ErrorException] exception that stores this information. The return type [bool] is the one specified in the documentation for [set_error_handler]: the function would return [false] to let PHP handle the error normally. It never returns anything here because it always throws an exception;
  • line 14: the key [firstName] does not exist. Instead of a warning, a [ErrorException] exception is thrown, which is caught (line 2 of the results);
  • line 17: the method [getSeverity], specific to the class [ErrorException], returns the level of the original error (line 3);
  • line 21: Attempting to open a nonexistent file also results in an exception (line 4);
  • line 26: restores the previous error handler, in this case that of PHP.

This technique is used by many PHP frameworks (Symfony, Laravel): during development, any warning becomes an exception, which forces the developer to fix the code. The [PHPUnit] testing tool that we’ll use later works in a similar way: it can cause a test to fail if it produces a warning or a deprecation.