Skip to content

7. Les exceptions et erreurs

Lorsqu'une méthode d'une classe rencontre une erreur irrécupérable (fichier inexistant, base de données non connectée, connexion réseau hors service), elle n'affiche pas une erreur sur une console (fichier, base de données) mais lance une exception. Les exceptions étendent la classe [\Exception]. Outre des exceptions, le fonctionnement interne de PHP lance également des erreurs dont la classe de base est la classe [\Error]. Les deux classes implémentent l'interface PHP [\Throwable].

PHP 8 a profondément changé la gestion des erreurs : de nombreuses situations qui produisaient en PHP 7 un simple avertissement (et un résultat souvent absurde) lancent désormais une erreur que l'on peut intercepter. Nous le verrons au fil des exemples.

7.1. L'arborescence des scripts

Les scripts de ce chapitre sont dans le dossier [scripts-console/exceptions] :

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. L'interface [\Throwable]

L'interface [\Throwable] est la suivante (cf. 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;
}

Le rôle des méthodes de l'interface est le suivant :

Méthode
Rôle
[getMessage]
rend le message d'erreur
[getCode]
rend le code de l'erreur (un entier)
[getFile]
rend le nom du fichier où l'objet a été créé
[getLine]
rend le numéro de la ligne où l'objet a été créé
[getTrace]
rend la pile des appels de fonctions sous la forme d'un tableau
[getTraceAsString]
rend la pile des appels de fonctions sous la forme d'une chaîne de caractères
[getPrevious]
rend l'objet [Throwable] précédent (encapsulé), ou [null]
[__toString]
rend une représentation de l'objet sous forme de chaîne de caractères

On remarquera que l'interface [Throwable] étend l'interface [Stringable] rencontrée au chapitre sur les classes. Une classe ne peut pas implémenter directement l'interface [Throwable] : elle doit dériver de [\Exception] ou de [\Error].

7.3. Les exceptions et erreurs prédéfinies dans PHP 8

Le script [throwables.php] suivant affiche l'arborescence des classes prédéfinies qui implémentent l'interface [Throwable]. Nous l'utilisons pour obtenir une liste exacte, celle de PHP 8.5, plutôt que de la recopier d'une documentation. Le lecteur ne perdra rien s'il ne cherche pas à comprendre son code, qui utilise l'API de réflexion de PHP (la classe [ReflectionClass] permet d'interroger PHP sur une classe : son nom, son extension d'origine, ses interfaces...) et une fonction récursive :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// affiche l'arborescence des classes prédéfinies implémentant l'interface [Throwable]
// on se limite aux extensions toujours présentes dans PHP 8.5 :
// le noyau (Core), la bibliothèque standard, la bibliothèque SPL, jSON et Random
$extensions = ["Core", "standard", "SPL", "json", "random"];

// les classes prédéfinies de ces extensions qui implémentent [Throwable]
$classes = array_filter(
    get_declared_classes(),
    function (string $classe) use ($extensions): bool {
        $reflexion = new ReflectionClass($classe);
        return $reflexion->isInternal()
            && $reflexion->implementsInterface(Throwable::class)
            && in_array($reflexion->getExtensionName(), $extensions, true);
    }
);
sort($classes);

// affichage récursif des classes filles de la classe [$parent]
function afficher(?string $parent, array $classes, string $indentation): void
{
    // les filles directes de [$parent]
    $filles = array_values(array_filter(
        $classes,
        fn(string $classe): bool => (get_parent_class($classe) ?: null) === $parent
    ));
    foreach ($filles as $i => $fille) {
        $derniere = $i === count($filles) - 1;
        print $indentation . ($derniere ? "└── " : "├── ") . $fille . "\n";
        afficher($fille, $classes, $indentation . ($derniere ? "    " : "│   "));
    }
}

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

Résultats :

> 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

Commentaires

  • ligne 13 : la fonction [get_declared_classes] rend la liste de toutes les classes connues à cet instant, prédéfinies ou non ;
  • lignes 16-18 : on ne garde que les classes prédéfinies (internal), qui implémentent l'interface [Throwable] et qui appartiennent aux extensions toujours présentes dans PHP 8.5 (ligne 9). D'autres extensions ajoutent leurs propres exceptions (par exemple [PDOException] pour l'accès aux bases de données, que nous rencontrerons plus tard) ;
  • lignes 24-36 : une fonction récursive qui affiche les classes filles directes de la classe [$parent], puis les filles de chacune d'elles, etc. La fonction [get_parent_class] rend [false] pour une classe sans parent : l'opérateur [?:] transforme alors [false] en [null] ;
  • ligne 29 : une fonction fléchée (arrow function, PHP 7.4), forme courte d'une fonction anonyme réduite à une expression ;
  • ligne 39 : on part des classes sans parent : [Error] et [Exception].

Les résultats montrent deux familles :

  • la famille [Exception] : ce sont les exceptions que lancent les bibliothèques et le code applicatif. On y trouve :
    • [ErrorException], qui permet de transformer un avertissement de PHP en exception (nous l'utiliserons en fin de chapitre) ;
    • [JsonException] (PHP 7.3), lancée par [json_encode] et [json_decode] avec l'option [JSON_THROW_ON_ERROR] ;
    • [Random\RandomException] (PHP 8.2), lancée par les fonctions de génération de nombres aléatoires ;
    • les exceptions de la bibliothèque SPL (Standard PHP Library), une collection de classes et d'interfaces destinées à résoudre des problèmes rencontrés fréquemment par les développeurs. Elles forment deux sous-familles :
    • [LogicException] et ses dérivées : des erreurs de programmation, qu'une correction du code doit faire disparaître : argument invalide ([InvalidArgumentException]), valeur hors du domaine attendu ([DomainException]), méthode inexistante ([BadMethodCallException])... ;
    • [RuntimeException] et ses dérivées : des erreurs qui ne peuvent être détectées qu'à l'exécution : indice hors bornes ([OutOfBoundsException]), valeur inattendue ([UnexpectedValueException]), dépassement de capacité ([OverflowException])... ;
  • la famille [Error] : ce sont les erreurs lancées par le moteur de PHP lui-même. On y trouve :
    • [TypeError] : une valeur n'a pas le type attendu (paramètre de fonction, attribut typé, valeur de retour, opérande d'un opérateur...). Sa dérivée [ArgumentCountError] signale un nombre d'arguments insuffisant ;
    • [ValueError] (PHP 8.0) : une valeur a le bon type mais une valeur invalide (par exemple une longueur négative ou nulle) ;
    • [ArithmeticError] et sa dérivée [DivisionByZeroError] : erreurs dans les opérations arithmétiques ;
    • [UnhandledMatchError] (PHP 8.0) : aucune branche d'une expression [match] ne convient ;
    • [AssertionError] : échec d'une assertion [assert] ;
    • [CompileError] et sa dérivée [ParseError] : erreur de syntaxe dans un fichier chargé par [require] ou [include] ;
    • [FiberError] (PHP 8.1) et [Random\RandomError] (PHP 8.2) : erreurs liées aux fibres et aux générateurs aléatoires, que nous n'utiliserons pas .

Les deux familles n'ont aucun lien de parenté : une clause [catch (\Exception $e)] n'intercepte pas une erreur [Error]. Seule l'interface [Throwable] les réunit.

7.4. Exemple 1

Le premier exemple [exceptions-01.php] montre à la fois un avertissement PHP et une erreur :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// affichage de toutes les erreurs
error_reporting(E_ALL);
ini_set("display_errors", "1");
// code --------
$var = [];
// clé inconnue
print $var["abcd"];
// division par zéro
$var = 7 / 0;
var_dump($var);
// tableau à bornes fixes
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// indice en-dehors des bornes
$array[5] = 8;

Commentaires

  • ligne 7 : on demande à PHP de signaler toutes les erreurs. Le paramètre de la fonction [error_reporting] est le niveau d'erreurs signalées. C'est une combinaison des constantes suivantes (les principales) :
Constante
Signification
[E_ERROR]
erreur fatale à l'exécution : le script est arrêté
[E_WARNING]
avertissement : le script continue
[E_PARSE]
erreur de syntaxe détectée à la compilation
[E_NOTICE]
remarque : quelque chose qui pourrait être une erreur
[E_DEPRECATED]
utilisation d'une fonctionnalité dépréciée, qui disparaîtra dans une version future
[E_USER_WARNING], [E_USER_NOTICE], [E_USER_DEPRECATED]
les mêmes niveaux, déclenchés par le code avec la fonction [trigger_error]
[E_ALL]
tous les niveaux
  • la constante [E_STRICT], qui ne correspond plus à aucun message depuis PHP 8.0, est dépréciée depuis PHP 8.4, de même que l'utilisation du niveau [E_USER_ERROR] avec [trigger_error] : pour signaler une erreur fatale, le code lance une exception ;
  • ligne 8 : on demande d'afficher les erreurs sur la console. En production, on fait l'inverse : les erreurs sont écrites dans un fichier de logs et non affichées ;
  • ligne 12 : on accède à un élément inexistant du tableau [$var] ;
  • ligne 14 : on fait une division par zéro ;
  • ligne 17 : on crée une instance de la classe [SplFixedArray] de la bibliothèque SPL. Cette classe permet de créer un tableau à bornes fixes et à indices entiers ;
  • ligne 21 : on accède à un élément inexistant du tableau.

Résultats :

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

Commentaires des résultats

  • ligne 3 : accéder à une clé inexistante d'un tableau provoque un avertissement de niveau [E_WARNING] (c'était une simple remarque [E_NOTICE] en PHP 7). Cela n'interrompt pas l'exécution du script ;
  • lignes 5-8 : en PHP 7, diviser un nombre par zéro provoquait un avertissement [E_WARNING] et rendait la valeur [INF] (infini). En PHP 8, l'opérateur [/] lance une erreur de type [DivisionByZeroError]. Comme elle n'est pas interceptée, elle interrompt l'exécution du script : c'est une erreur fatale (Fatal error: Uncaught...). Le message indique la pile des appels (Stack trace) au moment de l'erreur ;
  • le code qui suit la division n'est pas exécuté : l'accès à un indice inexistant du tableau [SplFixedArray] n'a pas lieu. Nous le retrouverons dans les exemples suivants.

7.5. Gérer les exceptions

Le script [exceptions-02.php] montre comment gérer les exceptions :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// on affiche toutes les erreurs
error_reporting(E_ALL);
ini_set("display_errors", "1");
// on entoure le code par un try / catch
try {
    $var = [];
    // clé inconnue
    print $var["abcd"];
    // division par zéro
    $var = 7 / 0;
    var_dump($var);
    // tableau à bornes fixes
    $array = new \SplFixedArray(5);
    $array[1] = 2;
    $array[4] = "foo";
    // indice en-dehors des bornes
    $array[5] = 8;
    // vérification
    print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
    // \Throwable est l'interface implémentée par toutes les erreurs et exceptions
    // on affiche l'exception
    print "erreur, message : " . $ex->getMessage() . ", type : " . $ex::class . "\n";
}

Commentaires

  • le script est celui qui a été présenté au paragraphe précédent. Seulement maintenant on a entouré le code susceptible de provoquer des erreurs par une structure [try / catch] : si le code des lignes 11-24 provoque (lance) une exception ou une erreur, celle-ci sera gérée par la clause [catch] des lignes 25-29 ;
  • ligne 25 : le paramètre de la clause [catch] est le type d'exception ou d'erreur que l'on veut gérer. En mettant comme type [\Throwable] qui est une interface, on indique qu'on veut gérer toute instance de classe implémentant l'interface [\Throwable]. Comme toutes les classes d'erreurs et d'exceptions implémentent cette interface, la clause [catch] gère ici toute erreur / exception ;
  • ligne 15 : l'instruction qui déclenche l'erreur. Dès qu'il y a exception, il y a branchement sur la clause [catch]. Le code qui suit dans le [try] n'est donc pas exécuté ;
  • ligne 28 : la notation [$ex::class] rend le nom de la classe de l'objet [$ex].

Résultats :

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
erreur, message : Division by zero, type : DivisionByZeroError

Commentaires des résultats

  • ligne 3 : on retrouve l'avertissement de niveau [E_WARNING]. Un avertissement n'est pas une exception et n'est donc pas géré par la clause [catch]. Nous verrons en fin de chapitre comment transformer les avertissements en exceptions ;
  • ligne 4 : on a le message d'erreur écrit dans la clause [catch]. Il s'est donc produit une exception dérivée de [\Exception] ou une erreur dérivée de [\Error]. Nous voyons ici qu'il s'agit de la classe [DivisionByZeroError]. Le script s'est terminé normalement.

7.6. Paramètres de la clause [catch]

Examinons le script [exceptions-03.php] suivant :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// on affiche toutes les erreurs
error_reporting(E_ALL);
ini_set("display_errors", "1");

// un tableau à bornes fixes
$array = new \SplFixedArray(5);
try {
    // indice en-dehors des bornes
    $array[5] = 8;
} catch (\Throwable $ex) {
    // affichage message d'erreur
    print "Erreur 1 : " . $ex->getMessage() . " (" . $ex::class . ")\n";
}

try {
    // indice en-dehors des bornes
    $array[5] = 8;
} catch (\Exception $ex) {
    // affichage message d'erreur
    print "Erreur 2 : " . $ex->getMessage() . "\n";
}

try {
    // indice en-dehors des bornes
    $array[5] = 8;
} catch (\RuntimeException $ex) {
    // affichage message d'erreur
    print "Erreur 3 : " . $ex->getMessage() . "\n";
}

try {
    // indice en-dehors des bornes
    $array[5] = 8;
} catch (\OutOfBoundsException $ex) {
    // affichage message d'erreur
    print "Erreur 4 : " . $ex->getMessage() . "\n";
}

try {
    // division par 0
    intdiv(5, 0);
} catch (\Throwable $ex) {
    // affichage message d'erreur
    print "Erreur 5 : " . $ex->getMessage() . " (" . $ex::class . ")\n";
}

try {
    // division par 0
    intdiv(5, 0);
} catch (\DivisionByZeroError $ex) {
    // affichage message d'erreur
    print "Erreur 6 : " . $ex->getMessage() . "\n";
}

try {
    // division par 0
    intdiv(5, 0);
} catch (\ArithmeticError $ex) {
    // affichage message d'erreur
    print "Erreur 7 : " . $ex->getMessage() . "\n";
}

try {
    // division par 0
    intdiv(5, 0);
} catch (\Error $ex) {
    // affichage message d'erreur
    print "Erreur 8 : " . $ex->getMessage() . "\n";
}

try {
    // division par 0
    intdiv(5, 0);
} catch (\Exception $ex) {
    // affichage message d'erreur
    print "Erreur 9 : " . $ex->getMessage() . "\n";
}

Commentaires

  • lignes 11-41 : 4 façons différentes de gérer l'exception générée par l'utilisation d'un indice incorrect avec la classe [\SplFixedArray]. L'exécution montre que cette erreur génère une exception de type [OutOfBoundsException] (ligne 2 des résultats). Dans le cours PHP 7 (et encore en PHP 8.3), c'était une exception [RuntimeException]. Le changement n'a pas cassé le code existant puisque la classe [OutOfBoundsException] dérive de la classe [RuntimeException] ;
    • ligne 15 : gère une erreur de type [\Throwable]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\Exception] qui implémente l'interface [\Throwable] ;
    • ligne 23 : gère une erreur de type [\Exception]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\Exception] ;
    • ligne 31 : gère une erreur de type [\RuntimeException]. C'est valide puisque le type [OutOfBoundsException] dérive du type [\RuntimeException]. C'est un bon choix : le code fonctionne quelle que soit la version de PHP ;
    • ligne 39 : gère le type exact de l'exception générée ;
  • lignes 46-81 : 5 façons différentes de gérer l'erreur générée par la fonction [intdiv] lorsqu'on passe à celle-ci un diviseur égal à 0. La fonction [intdiv(int $dividend, int $divisor): int] fait la division entière [$dividend / $divisor]. Lorsque le diviseur est nul, l'erreur [\DivisionByZeroError] est lancée ;
    • ligne 47 : on intercepte toute erreur implémentant l'interface [\Throwable]. C'est valide ;
    • ligne 55 : on intercepte le type exact de l'erreur : c'est la méthode à privilégier ;
    • ligne 63 : on intercepte le type [\ArithmeticError]. C'est valide puisque la classe [DivisionByZeroError] étend la classe [ArithmeticError] ;
    • ligne 71 : on intercepte le type [\Error]. C'est valide puisque la classe [DivisionByZeroError] dérive de la classe [Error] ;
    • ligne 79 : on intercepte le type [\Exception]. C'est invalide car la classe [DivisionByZeroError] n'a aucun lien avec la classe [\Exception] .

Résultats :

> php exceptions-03.php
Erreur 1 : Index invalid or out of range (OutOfBoundsException)
Erreur 2 : Index invalid or out of range
Erreur 3 : Index invalid or out of range
Erreur 4 : Index invalid or out of range
Erreur 5 : Division by zero (DivisionByZeroError)
Erreur 6 : Division by zero
Erreur 7 : Division by zero
Erreur 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

Les lignes 11-15 des résultats montrent que l'erreur [DivisionByZeroError] n'a pas été interceptée par la dernière clause [catch (\Exception $ex)] : c'est une erreur fatale.

7.7. Clause [finally]

La structure [try / catch] peut avoir un troisième élément et devenir une structure [try / catch / finally]. Le code de la clause [finally] est exécuté dans les deux cas suivants :

  • la clause [try] ne lance pas d'exception. Elle est alors exécutée entièrement puis l'exécution du code passe à la clause [finally] qui est exécutée entièrement ;
  • la clause [try] lance une exception. Elle est alors exécutée jusqu'à l'instruction qui lance l'exception. L'exécution du code passe alors à la clause [catch] qui est exécutée entièrement. Puis l'exécution du code passe à la clause [finally] qui est exécutée entièrement .

Finalement, le code de la clause [finally] est toujours exécuté. Ce scénario est utile dans le cas suivant :

  • dans le [try], le code a obtenu des ressources (fichiers, bases de données, connexions réseau, files d'attente). En général ces ressources sont coûteuses. Il faut alors les rendre (on dit le plus souvent fermer) dès que c'est possible ;
  • si l'acquisition des ressources a été faite dans le [try], on mettra leur restitution dans le [finally]. Cela nous assure que dans tous les cas (erreur ou pas), les ressources acquises sont restituées au système .

Le script suivant [exceptions-04.php] nous montre le fonctionnement de la clause [finally] dans diverses situations :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// on crée une instance d'exception
$e = new \Exception("Erreur...");

// premier test
try {
    print "Premier test\n";
    throw $e;
} catch (\Exception $ex1) {
    print $ex1->getMessage() . "\n";
} finally {
    print "Terminé\n";
}

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

// troisième test
try {
    print "Troisième test\n";
    return;
} catch (\Exception $ex1) {
    print $ex1->getMessage() . "\n";
} finally {
    print "Terminé\n";
}

Commentaires du code

  • ligne 7 : [$e] est une instance de la classe prédéfinie [\Exception]. On va la lancer à différents endroits ;
  • lignes 11-12 : l'exception [$e] est lancée dans le [try] avec l'instruction [throw] ;
  • ligne 13 : l'exception [\Exception] est interceptée et son message d'erreur écrit sur la console ;
  • lignes 15-16 : la clause [finally] écrit un message. D'après ce qui a été dit précédemment, ce message devrait être tout le temps écrit, erreur ou pas dans le [try] ;
  • ligne 21 : il n'y a pas d'erreur dans le [try]. On devrait là également passer dans le [finally] ;
  • ligne 31 : il y a une instruction [return] dans le [try] et pas d'erreur. On peut se demander alors si on va passer dans la clause [finally]. L'exécution montre que oui. Utilisée en-dehors d'une fonction, l'instruction [return] termine le script.

Résultats :

1
2
3
4
5
6
7
8
> php exceptions-04.php
Premier test
Erreur...
Terminé
Second test
Terminé
Troisième test
Terminé

Examinons un autre cas [exceptions-05.php] :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// quatrième test
try {
    print "Quatrième test\n";
    exit;
} finally {
    print "Terminé\n";
}

Commentaires

  • ligne 9 : l'instruction [exit] arrête immédiatement l'exécution du script : la clause [finally] n'est pas exécutée ;
  • lignes 7-12 : un exemple de [try / finally] sans clause [catch]. C'est possible.

Résultats :

> php exceptions-05.php
Quatrième test

7.8. Créer ses propres classes d'exceptions

Dans un projet un peu important, il est utile de différencier les différentes erreurs en les encapsulant dans différentes classes d'exceptions. Dans les scripts précédents, nous avons vu que toute exception pouvait être interceptée par une clause [catch (\Throwable $ex)]. C'est recommandé si on n'a aucune idée de l'erreur interceptée et que le traitement est le même pour toutes les erreurs. C'est parfois le cas mais on a souvent besoin d'adapter le traitement au type exact de l'erreur. Il faut alors différencier les erreurs entre elles.

Examinons le script [exceptions-06.php] suivant :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// on définit notre propre famille d'exceptions
class Exception1 extends \RuntimeException
{
}

class Exception2 extends \RuntimeException
{
}

// on utilise nos exceptions
$e1 = new Exception1("Erreur1...");
var_dump($e1);
$e2 = new Exception2("Erreur2...", 2);

// premier test
print "premier test\n";
try {
    // on lance un type Exception1
    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 {
    // on lance un type Exception2
    throw $e2;
} catch (Exception1 $ex1) {
    print "Exception 1\n";
    print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
    print "Exception 2\n";
    print $ex2->getMessage() . "\n";
}

// troisième test
print "troisième test\n";
try {
    // on lance un type Exception1
    throw $e1;
} catch (Exception1 | Exception2 $ex) {
    print "Exception 1 ou 2\n";
    print $ex->getMessage() . "\n";
}

// quatrième test
print "quatrième test\n";
try {
    // on lance un type Exception2
    throw $e2;
} catch (Exception1 | Exception2 $ex) {
    print "Exception 1 ou 2 : " . $ex::class . "\n";
    // les informations portées par l'exception
    print "message : " . $ex->getMessage() . "\n";
    print "code : " . $ex->getCode() . "\n";
    print "fichier : " . basename($ex->getFile()) . "\n";
    print "ligne : " . $ex->getLine() . "\n";
}

Commentaires

  • lignes 7-9 : on définit deux classes [Exception1] et [Exception2] toutes deux dérivées de la classe prédéfinie [\RuntimeException]. Le corps de ces classes est vide. Autrement dit, on ne les utilise que pour leurs types : c'est parce qu'elles ont des types différents qu'on va pouvoir différencier ces deux exceptions dans les clauses [catch] ;
  • lignes 16-18 : on définit deux variables [$e1] et [$e2] ayant respectivement les types [Exception1] et [Exception2]. Le constructeur utilisé est celui de la classe [Exception] dont la signature est [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. Pour [$e2], on a précisé un code d'erreur ;
  • lignes 21-31 : on a une structure [try / catch / catch]. Cela permet de gérer différents types d'exception avec différentes clauses [catch]. La première clause dont le type convient est exécutée, les autres sont ignorées ;
  • ligne 25 : intercepte les exceptions de type [Exception1] ;
  • ligne 28 : intercepte les exceptions de type [Exception2] ;
  • ligne 51 : intercepte les exceptions de type [Exception1] ou ([|]) [Exception2]. Cette syntaxe existe depuis PHP 7.1 ;
  • ligne 62 : le nom de la classe de l'exception interceptée ;
  • lignes 64-67 : les informations portées par l'exception, obtenues par les méthodes de l'interface [Throwable]. La fonction [basename] rend le nom d'un fichier sans le chemin de son dossier.

Résultats :

> php exceptions-06.php
object(Exception1)#1 (7) {
  ["message":protected]=>
  string(10) "Erreur1..."
  ["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
}
premier test
Exception 1
Erreur1...
second test
Exception 2
Erreur2...
troisième test
Exception 1 ou 2
Erreur1...
quatrième test
Exception 1 ou 2 : Exception2
message : Erreur2...
code : 2
fichier : exceptions-06.php
ligne : 18

Commentaires des résultats

  • lignes 2-18 : le « contenu » d'une exception, affiché par [var_dump] :
    • lignes 3-4 : le message d'erreur ;
    • lignes 7-8 : le code d'erreur ;
    • lignes 9-10 : le nom du fichier dans lequel l'exception a été créée. La longueur indiquée (string(76)) est celle du chemin complet du fichier : elle dépend du dossier dans lequel se trouve le script ;
    • lignes 11-12 : la ligne à laquelle l'exception a été créée ;
    • lignes 13-15 : la pile des appels de fonctions au moment de la création de l'exception. Elle est vide ici car l'exception a été créée dans le script principal et non dans une fonction ;
    • lignes 16-17 : l'exception précédente. Une exception peut encapsuler une autre exception et définir ainsi une pile d'exceptions. L'attribut [previous] va permettre d'exploiter cette pile ;
  • lignes 29-33 : on remarquera que le fichier et la ligne de l'exception sont ceux de sa création par [new] (ligne 18) et non ceux de son lancement par [throw].

7.9. Relancer une exception

Une exception peut être lancée plusieurs fois comme le montre le script [exceptions-07.php] suivant :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

try {
    try {
        // on lance une exception
        throw new \Exception("test");
    } catch (\Exception $ex) {
        // on relance l'exception interceptée
        throw $ex;
    } finally {
        // on passera bien dans le finally
        print "finally 1\n";
    }
} catch (\Exception $ex2) {
    // on récupère bien l'exception initiale
    print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
    // on passera bien dans le finally
    print "finally 2\n";
}

Commentaires

  • ligne 9 : on lance une exception ;
  • ligne 10 : on l'intercepte ;
  • ligne 12 : on la relance. Elle passe alors dans la structure [try / catch / finally] du niveau supérieur. Ce scénario est courant : un code intercepte une exception pour faire un traitement local (écrire un log, annuler une transaction...) puis la relance pour que le code appelant soit lui aussi prévenu ;
  • ligne 17 : on l'intercepte de nouveau ;
  • lignes 13-15 : l'exécution montre que même après le [throw] de la ligne 12, on passe bien dans la clause [finally] du [try / catch / finally] interne.

Résultats :

1
2
3
4
> php exceptions-07.php
finally 1
test dans try / catch / finally externe
finally 2

7.10. Exploitation d'une pile d'exceptions

Une exception peut encapsuler une autre exception qui elle-même peut en encapsuler une autre formant finalement une pile d'exceptions. Voici un exemple [exceptions-08.php] :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// on définit notre propre famille d'exceptions
class Exception1 extends \RuntimeException
{
}

class Exception2 extends \RuntimeException
{
}

class Exception3 extends \RuntimeException
{
}

// on utilise nos exceptions
$e1 = new Exception1("Erreur 1...", 1, new Exception2("Erreur 2...", 2, new Exception3("Erreur 3...")));
var_dump($e1);
// exploitation de la pile d'exceptions
$e = $e1;
do {
    // message d'erreur de l'exception courante
    print $e::class . " : " . $e->getMessage() . "\n";
    // on passe à l'exception précédente (null s'il n'y en a pas)
    $e = $e->getPrevious();
} while ($e !== null);

Commentaires

  • lignes 7-15 : définissent trois classes d'exceptions dérivées de l'exception prédéfinie [RuntimeException] ;
  • ligne 20 : une instance de la classe [Exception3] est encapsulée dans une instance de la classe [Exception2] qui elle-même est encapsulée dans une instance de la classe [Exception1]. Le constructeur utilisé ici est le constructeur de la classe [Exception] : [__construct(string $message = "", int $code = 0, ?Throwable $previous = null)]. Le 3e paramètre du constructeur permet d'encapsuler une exception. Cela peut être utile dans le scénario suivant :
    • on définit une méthode M qui peut générer une exception de type [Exception1] et seulement de ce type, pour des raisons de compatibilité par exemple avec une interface ;
    • or dans la méthode M peuvent se produire d'autres types d'exceptions. Pour remonter une erreur au code appelant la méthode M, on encapsulera alors ces exceptions dans le type [Exception1] que l'on lancera. Cela permet de ne pas perdre l'information contenue dans l'exception encapsulée et qui a été la cause originelle de l'erreur ;
  • lignes 24-29 : montrent comment parcourir la pile des exceptions internes à une exception. La méthode [getPrevious] rend l'exception encapsulée ou [null] s'il n'y en a pas. La boucle [do ... while] exécute son corps au moins une fois puis tant que la condition est vraie.

Résultats :

> php exceptions-08.php
object(Exception1)#1 (7) {
  ["message":protected]=>
  string(11) "Erreur 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(11) "Erreur 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(11) "Erreur 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 : Erreur 1...
Exception2 : Erreur 2...
Exception3 : Erreur 3...
  • lignes 2-50 : l'exception [$e1] contient dans son attribut [previous] l'exception [Exception2] (lignes 17-49) qui elle-même contient l'exception [Exception3] (lignes 32-48) ;
  • lignes 51-53 : le parcours de la pile des exceptions.

7.11. Des avertissements de PHP 7 devenus des erreurs en PHP 8

Nous avons vu que la division par zéro, qui produisait un avertissement en PHP 7, lance une erreur en PHP 8. Ce n'est pas un cas isolé : PHP 8 a transformé en erreurs de nombreux avertissements dont l'exécution continuait avec un résultat faux. Le script [exceptions-09.php] en présente quelques-uns :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// affiche le type et le message d'une erreur
function afficher(int $n, \Throwable $ex): void
{
    print "$n - " . $ex::class . " : " . $ex->getMessage() . "\n";
}

// une fonction à deux paramètres
function somme(int $a, int $b): int
{
    return $a + $b;
}

// 1 - variable non définie : un simple avertissement (Notice en PHP 7, Warning en PHP 8)
print "1 - [" . $inconnue . "]\n";
// 2 - modulo par zéro : avertissement + résultat faux en PHP 7, erreur en PHP 8
try {
    print 1 % 0;
} catch (\DivisionByZeroError $ex) {
    afficher(2, $ex);
}
// 3 - opération arithmétique sur un tableau
try {
    print [] + 1;
} catch (\TypeError $ex) {
    afficher(3, $ex);
}
// 4 - opération arithmétique sur une chaîne non numérique
try {
    print "abc" * 2;
} catch (\TypeError $ex) {
    afficher(4, $ex);
}
// 5 - mauvais type de paramètre pour une fonction prédéfinie
try {
    print strlen([1, 2]);
} catch (\TypeError $ex) {
    afficher(5, $ex);
}
// 6 - paramètre du bon type mais de valeur invalide
try {
    print_r(array_chunk([1, 2, 3], 0));
} catch (\ValueError $ex) {
    afficher(6, $ex);
}
// 7 - nombre d'arguments insuffisant
try {
    print somme(1);
} catch (\ArgumentCountError $ex) {
    afficher(7, $ex);
}
// 8 - expression [match] sans branche correspondante (PHP 8.0)
try {
    $jour = 8;
    print match ($jour) {
        6, 7 => "week-end",
        1, 2, 3, 4, 5 => "semaine",
    };
} catch (\UnhandledMatchError $ex) {
    afficher(8, $ex);
}
// 9 - méthode appelée sur null
try {
    $objet = null;
    $objet->methode();
} catch (\Error $ex) {
    afficher(9, $ex);
}
// 10 - l'opérateur nullsafe ?-> évite l'erreur précédente : le résultat est null
var_dump($objet?->methode());

Résultats :

> php exceptions-09.php

Warning: Undefined variable $inconnue 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 somme(), 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 methode() on null
NULL

Commentaires

  • ligne 19 : l'utilisation d'une variable non définie reste un simple avertissement, mais de niveau [E_WARNING] (c'était une remarque [E_NOTICE] en PHP 7). La variable vaut [null] et le script continue (lignes 2-4 des résultats) ;
  • ligne 22 : le modulo par zéro lance une erreur [DivisionByZeroError] (ligne 5). C'était déjà le cas en PHP 7 : seule la division [/] par zéro y produisait un simple avertissement ;
  • ligne 28 : une opération arithmétique sur un tableau provoque une erreur [TypeError] (ligne 6) ;
  • ligne 34 : en PHP 7, une opération arithmétique sur une chaîne non numérique provoquait l'avertissement « A non-numeric value encountered » et la chaîne valait 0. En PHP 8, c'est une erreur [TypeError] (ligne 7). Une chaîne numérique (["12" * 2]) reste acceptée ;
  • ligne 40 : en PHP 7, passer un paramètre d'un mauvais type à une fonction prédéfinie provoquait un avertissement et la fonction rendait [null]. En PHP 8, toutes les fonctions prédéfinies lancent une erreur [TypeError], comme les fonctions écrites par le développeur (ligne 8). Le message désigne le paramètre par son numéro et son nom ;
  • ligne 46 : le paramètre a le bon type ([int]) mais une valeur invalide (une longueur doit être positive) : c'est une erreur [ValueError], nouvelle en PHP 8.0 (ligne 9). En PHP 7, c'était un avertissement ;
  • ligne 52 : la fonction [somme] attend deux arguments et n'en reçoit qu'un : erreur [ArgumentCountError] (ligne 10) ;
  • lignes 59-62 : l'expression [match] n'a pas de branche pour la valeur 8 et n'a pas de branche [default] : erreur [UnhandledMatchError] (ligne 11). C'est une différence importante avec l'instruction [switch] qui, elle, ne fait rien lorsqu'aucun [case] ne convient ;
  • ligne 69 : l'appel d'une méthode sur [null] est une erreur [Error] (ligne 12). C'était déjà le cas en PHP 7 ;
  • ligne 74 : l'opérateur nullsafe [?->] (PHP 8.0) rend [null] si l'objet est [null], au lieu d'appeler la méthode. On évite ainsi d'écrire un test [if ($objet !== null)] (ligne 13).

Ces changements rendent les programmes PHP 8 plus sûrs : une erreur interrompt le traitement au lieu de le laisser continuer avec des données fausses. Ils obligent aussi le développeur à prévoir les clauses [catch] correspondantes.

7.12. [catch] multiple, [catch] sans variable et [throw] expression

Le script [exceptions-10.php] présente trois facilités de PHP 8 et un usage courant de l'encapsulation d'exceptions :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// [throw] est une expression depuis PHP 8.0 : on peut l'utiliser après ?? ou dans une fonction fléchée
function lireAge(array $data): int
{
    $age = $data["age"] ?? throw new \InvalidArgumentException("L'âge est absent");
    return is_int($age) && $age >= 0 ? $age : throw new \DomainException("Âge [$age] invalide");
}

// une fonction fléchée qui ne fait que lancer une exception
$interdit = fn(string $operation) => throw new \LogicException("Opération [$operation] interdite");

// des données de test
$tests = [["age" => 48], [], ["age" => -4]];
foreach ($tests as $i => $data) {
    try {
        print "test $i : âge=" . lireAge($data) . "\n";
    } catch (\InvalidArgumentException | \DomainException $ex) {
        // une seule clause [catch] pour deux types d'exceptions
        print "test $i : " . $ex::class . " : " . $ex->getMessage() . "\n";
    }
}

try {
    $interdit("suppression");
} catch (\LogicException) {
    // depuis PHP 8.0, la variable de la clause [catch] est facultative
    print "l'opération a été refusée\n";
}

// encapsuler une exception de bas niveau dans une exception de l'application
class DaoException extends \RuntimeException
{
}

function lireConfiguration(string $json): array
{
    try {
        return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
    } catch (\JsonException $ex) {
        // la cause originelle est passée en 3e paramètre : elle n'est pas perdue
        throw new DaoException("Configuration illisible", 10, $ex);
    }
}

try {
    lireConfiguration('{"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";
}

Résultats :

1
2
3
4
5
6
7
> php exceptions-10.php
test 0 : âge=48
test 1 : InvalidArgumentException : L'âge est absent
test 2 : DomainException : Âge [-4] invalide
l'opération a été refusée
DaoException : Configuration illisible (code 10)
cause : JsonException : Syntax error

Commentaires

  • ligne 9 : depuis PHP 8.0, [throw] est une expression et non plus seulement une instruction. On peut donc l'utiliser là où une valeur est attendue : ici, après l'opérateur [??]. Si la clé [age] n'existe pas, l'exception est lancée ; sinon [$age] reçoit sa valeur ;
  • ligne 10 : [throw] utilisé dans une expression conditionnelle [? :] ;
  • ligne 14 : une fonction fléchée dont le corps se réduit à lancer une exception. Avant PHP 8.0, c'était impossible : une fonction fléchée ne peut contenir qu'une expression ;
  • ligne 21 : une seule clause [catch] pour deux types d'exceptions (lignes 3-4 des résultats). On remarquera que [InvalidArgumentException] et [DomainException] dérivent toutes deux de [LogicException] : un [catch (\LogicException $ex)] aurait eu le même effet ;
  • ligne 29 : depuis PHP 8.0, la variable de la clause [catch] est facultative lorsqu'on n'a pas besoin de l'exception, seulement de son type (ligne 5) ;
  • lignes 35-37 : une exception propre à l'application. Dans les chapitres suivants, nous écrirons des couches d'accès aux données (DAO, Data Access Object) qui ne lanceront que ce type d'exception, quelle que soit l'origine de l'erreur (fichier, jSON, base de données) ;
  • lignes 39-47 : la fonction [lireConfiguration] décode une chaîne jSON. L'exception de bas niveau [JsonException] est interceptée et encapsulée (ligne 45) dans une exception [DaoException]. La cause originelle est passée en 3e paramètre du constructeur ;
  • ligne 53 : le code appelant ne connaît que [DaoException], mais il peut retrouver la cause originelle de l'erreur avec la méthode [getPrevious] (lignes 6-7 des résultats).

7.13. Transformer les avertissements en exceptions : [ErrorException]

Malgré les progrès de PHP 8, certaines anomalies restent de simples avertissements : clé de tableau inexistante, variable non définie, fichier impossible à ouvrir avec [fopen]... Ces avertissements ne peuvent être interceptés par un [try / catch]. On peut cependant installer un gestionnaire d'erreurs qui les transforme en exceptions [ErrorException]. C'est ce que fait le script [exceptions-11.php] :

<?php

// respect strict des types déclarés des paramètres de fonctions
declare(strict_types=1);

// gestionnaire d'erreurs : transforme les avertissements de PHP en exceptions [ErrorException]
set_error_handler(function (int $niveau, string $message, string $fichier, int $ligne): bool {
    throw new \ErrorException($message, 0, $niveau, $fichier, $ligne);
});

// une clé inconnue ne provoque plus un simple avertissement mais une exception
$personne = ["nom" => "Langevin"];
try {
    print $personne["prenom"] . "\n";
} catch (\ErrorException $ex) {
    print "Erreur : " . $ex->getMessage() . " (ligne " . $ex->getLine() . ")\n";
    print "niveau E_WARNING : " . var_export($ex->getSeverity() === E_WARNING, true) . "\n";
}
// idem pour l'ouverture d'un fichier inexistant
try {
    $flux = fopen(__DIR__ . "/inexistant.txt", "r");
} catch (\ErrorException $ex) {
    print "Erreur : " . $ex->getMessage() . "\n";
}
// on revient au gestionnaire d'erreurs de PHP
restore_error_handler();
print "fin\n";

Résultats :

1
2
3
4
5
> php exceptions-11.php
Erreur : Undefined array key "prenom" (ligne 14)
niveau E_WARNING : true
Erreur : fopen(.../php8-oct-2026/scripts-console/exceptions/inexistant.txt): Failed to open stream: No such file or directory
fin

Commentaires

  • lignes 7-9 : la fonction [set_error_handler] installe un gestionnaire d'erreurs : une fonction que PHP appellera à chaque avertissement, remarque ou dépréciation, au lieu d'afficher son message. Elle reçoit le niveau de l'erreur, son message, le fichier et la ligne où elle s'est produite. Ici, elle lance une exception [ErrorException] qui mémorise ces informations. Le type de retour [bool] est celui que prévoit la documentation de [set_error_handler] : la fonction rendrait [false] pour laisser PHP traiter l'erreur normalement. Elle ne rend jamais rien ici puisqu'elle lance toujours une exception ;
  • ligne 14 : la clé [prenom] n'existe pas. Au lieu d'un avertissement, on obtient une exception [ErrorException] que l'on intercepte (ligne 2 des résultats) ;
  • ligne 17 : la méthode [getSeverity], propre à la classe [ErrorException], rend le niveau de l'erreur d'origine (ligne 3) ;
  • ligne 21 : l'ouverture d'un fichier inexistant devient également une exception (ligne 4) ;
  • ligne 26 : rétablit le gestionnaire d'erreurs précédent, ici celui de PHP.

Cette technique est utilisée par de nombreux frameworks PHP (Symfony, Laravel) : en développement, tout avertissement devient une exception, ce qui oblige à corriger le code. L'outil de tests [PHPUnit] que nous utiliserons plus loin procède de façon analogue : il peut faire échouer un test qui produit un avertissement ou une dépréciation.