La gestion des exceptions et le contrôle d'erreurs sous Delphi

Tony Baheux

Tout d'abord qu'est qu'une exception ? Comme son nom l'indique, une exception est un événement imprévu par le programmeur (ou programme). Vous me direz comment peut-il gérer quelque chose qu'il n'a pas prévu ? Très simple, le programmeur sachant que le monde n'est pas parfait aura pris le soin de protéger des blocs d'instructions sensibles.

- Titre : La gestion des exceptions et le contrôle d'erreurs sous Delphi

- Auteur : Tony Baheux

- Parution : 1 janvier 2005

- Mise à jour : 20 décembre 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Tony Baheux. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Try..Finally

- III. Try..Except

- III-A. Grammaire

- III-B. Liste non exhaustive des classes d'exception

- IV. Exception personnalisée

- V. Raise

- VI. Conclusion

I - Introduction

Le présent tutoriel n'a pas la prétention de tout vous apprendre sur la gestion des exceptions (comme tous les tutoriels que je fais, mais celui-là plus que les autres). Mon but est de vous aider à ne plus rechigner à les utiliser, en vous donnant tout ce que vous avez besoin de savoir pour vous sentir à l'aise avec la bête. Pour ceux qui ont déjà pris l'habitude d'utiliser leurs services, je leur conseille de le parcourir quand même, il se pourrait qu'ils apprennent quelque chose. Si tel n'était pas le cas, toutes mes excuses (et mieux encore vous en connaissez plus sur le sujet, je vous invite à compléter/modifier ce tutoriel).

Tout d'abord qu'est qu'une exception ? Comme son nom l'indique, une exception est un événement imprévu par le programmeur (ou programme). Vous me direz comment peut-il gérer quelque chose qu'il n'a pas prévu ? Très simple, le programmeur sachant que le monde n'est pas parfait aura pris le soin de protéger des blocs d'instructions sensibles.

Concrètement, lorsque vous développez votre application, vous vous attendez à ce que telle ou telle chose soit disponible (un fichier dll par exemple) ou que l'utilisateur saisisse tel type de donnée dans telle variable (un champ de saisie où il faut entrer des nombres). Par utilisateur, je considère aussi bien une personne que le programme lui-même. Or il arrive que les choses ne se passent pas tout à fait comme vous les avez souhaitées, fichier absent, caractère vers une variable numérique, etc.
Pour protéger votre application et plus précisément un bloc de code, une solution est d'utiliser les exceptions et ainsi éviter à votre application de planter lamentablement. Une autre solution serait de faire des tests, mais si pour des cas simples, les tests sont faciles à mettre en place (l'exemple de la division par zéro est assez parlant), il n'en est pas de même pour des tâches plus complexes ou sujettes à différents types d'erreur (comme l'accès à un fichier). D'autres diront que gérer par exception les cas à problème, allège le code (à vous de voir). D'autre diront que c'est mieux de faire ainsi parce que c'est plus classe (là aussi à vous de voir)

Et justement, ce cours a pour but de vous permettre de vous faire une opinion :)

Les exceptions peuvent être de natures différentes suivant l'opération où elles se produisent. Ces différentes exceptions sont appelées type d'exception ou classe d'exception. Par exemple lors d'une tentative de conversion d'un string en float, si le string contient des lettres, on aura une exception de type classe de conversion (EConverError).

La protection d'un bloc de code se fait par l'utilisation d'un couple de mots clés. Ces couples sont : try..finally et try..except. Les deux couples protègent le code situé entre try et l'autre mot clé, si une exception arrive entre ces deux mots clés, le programme arrête de traiter le bloc protégé et passe tout de suite aux instructions qui suivent le second mot clé du couple. Un peu comme un if/else, sauf qu'au lieu du test, on surveille les erreurs éventuelles entre le if et le else. La différence entre le couple try..finally et try..except se situe dans la condition d'exécution du second bloc d'instruction et dans la suite de l'exécution du programme.

Les deux couples de mots clés sont utilisés de manière tout à fait différente, car ils ont une condition d'exécution différente et un comportement différent.

- Le couple try..finally est utilisé pour s'assurer qu'une opération sera toujours réalisée. En effet, peu importe qu'il y ait eu une erreur, les instructions entre le finally et son end; seront exécutées.
D'ailleurs, l'erreur n'est pas du tout traitée par un couple try..finally. L'unique but de ce couple est de garantir que le code entre finally et son end sera exécuté. Quant à l'erreur, si elle ne trouve pas un couple try..except, elle est remontée à la fonction appelante jusqu'à ce qu'elle soit traitée. Dans le pire des cas, l'erreur est affichée exactement comme lorsque le programme n'est pas protégé.

- Avec try..except, les instructions du second bloc d'instructions ne sont exécutées que s'il y a eu une erreur entre le try et le except. Le but de ce couple sert effectivement à gérer les erreurs. try..except propose un mécanisme pour traiter, l'exception en fonction de la classe d'exception. Autrement dit, de traiter l'erreur en fonction de sa nature.

Pour vous faire une idée plus concrète de la chose, voici la syntaxe pour try..finally et try..except.

Try..Finally

instructions1
try
// Bloc de code à protéger
instructions protégées
finally
// Second bloc de code
// Ce bloc sera exécuté à la fin des instructions protégées
// ou dès qu'une erreur survient dans le bloc de code protégé
instructions2
end;
// suite des instructions qui seront exécutées s'il n'y a pas eu d'erreur plus haut.
instructions3

Try..Except

instructions1
try
// Bloc de code à protéger
instructions protégées
except
// Second bloc de code
// Ce bloc ne sera exécuté que si une erreur survient dans la partie protégée
instructions2
end;
// suite des instructions qui seront exécutées, même en cas d'erreur plus haute, car déjà traitée.
instructions3

Vous l'aurez remarqué, le bloc dit protégé est celui qui le paraît le moins. En fait on devrait dire bloc à protéger, c'est-à-dire qu'il s'agit d'une suite d'instructions pouvant causer des exceptions. Votre programme ne va plus planter lamentablement lorsqu'il rencontrera une erreur dans un bloc protégé.

Par contre vous continuerez d'avoir les messages d'erreur en l'exécutant depuis Delphi, cela est destiné à vous aider à vérifier que vous interceptez bien la bonne classe d'exception. Nous verrons cela plus en détail pour la partie try..except.

II - Try..Finally

La syntaxe de try..finally est :

try
instruction1
finally
instruction2
end;

Instruction1 est une partie sensible du code (manipulation d'un fichier, création d'un objet, etc.) et instruction2 une partie du code qui doit être exécuté quoi qu'il arrive (libération de ressource, fermeture d'une connexion, etc.). Si une erreur survient dans les instructions du bloc instruction1, l'exécution passe immédiatement à l'exécution d'instruction2 sinon l'exécution termine les instructions et passe ensuite au instruction2.

Le programme poursuit ensuite normalement son exécution s'il n'y a pas eu d'erreur sinon l'erreur est remontée au prochain except. S'il n'y a aucun couple try..except pour gérer l'erreur, le programme se conduit comme d'habitude en cas d'erreur et vous gratifie d'un message sibyllin. (Rassurez-vous, vous aurez toutes les pièces en main avec la partie try..except.)

Vous l'aurez compris, son utilisation est fortement recommandée pour libérer une ressource même si le programme rencontre une erreur. Mais attention, l'instruction demandant la ressource doit se trouver à l'extérieur du try..finally. Dans le cas contraire, s'il arrivait que le programme ne puisse pas allouer la ressource, tenter de la libérer peut provoquer une erreur.

Exemple : Création d'un objet

procedure TForm1.Button1Click(Sender: TObject);
var
Ob : TObjetExemple;
begin
Ob := TObjetExemple.Create; // Création de l'objet, en dehors du try..finally
try
// Manipulation de l'objet
{instructions}
finally
Ob.Free; // Libération de l'objet
end;
end;

Exemple : assignation d'un fichier

function OuvrirF(Nom : TFileName) : boolean;
var
F : Textfile;
S : string;
i, j, valeur :integer;
begin
AssignFile(F,Nom); // Ouverture du fichier, en dehors du try..finally
try
// Manipulation du fichier
Reset(F);
readln(F,S);
{instructions}
Result := True;
finally
CloseFile(F); // Libération du fichier
end;
end;

Notez la position de demande d'allocation de ressource par rapport au try..finally.

Attention

Tant que vous n'avez pas appelé CloseFile(F), vous ne pouvez plus manipuler le fichier (renommer, détruire, déplacer, etc.). Ne l'oubliez pas ! Ceci est valable pour les fichiers, mais aussi pour d'autres ressources (base de données, périphérique…). D'où l'importance de s'assurer de leur libération.

III - Try..Except

III-A - Grammaire

Commençons par un simple et classique

Examinons une application fournissant un champ de saisie n'attendant que des nombres comme saisie. Deux solutions s'offrent à vous, la première empêcher l'utilisateur d'entrer autre chose que des caractères numériques et la seconde utiliser les exceptions. Laissons la première de côté et intéressons-nous à la seconde. Ce que nous devons protéger est le moment où la saisie de l'utilisateur doit être affectée à une variable de type numérique. Vous pouvez tester en réalisant une application faisant une telle opération. Lors de l'exécution, un message d'erreur se produira dès que vous affecterez des lettres à la variable, plantant le plus souvent votre application. Par contre en protégeant votre bloc de code, non seulement, vous limitez l'erreur à cette portion de code et vous pouvez en plus réaliser un traitement spécifique au problème (réinitialiser des variables, informer l'utilisateur…).

La syntaxe est la suivante :

try
instruction1
except
instruction2
end;
instruction3

Instruction1 est comme pour le try..finally la partie sensible du code tandis qu'instruction2 le code qui sera exécuté si instruction1 provoque une erreur. Si une erreur survient dans les instructions du bloc instruction1, l'exécution passe immédiatement à l'exécution d'instruction2 sinon l'exécution termine les instructions et passe ensuite aux instruction3. Contrairement au try..finally, la suite de l'exécution se passe normalement, l'erreur ayant été traitée.

Exemple : Gestion des erreurs liées à une conversion de Type.

procedure TForm1.Button1Click(Sender: TObject);
var
param1 : Double;
begin
try
param1 := StrToFloat(Edit1.Text);
{suite des instructions}
except
on EconvertError do
MessageDlg('Erreur : Vous devez entrer un réel'
+#10#13+'Le séparateur décimal est : '+DecimalSeparator, mtError, [mbOk], 0);
end;
{Autre instruction non sensible}
end;

Essayez cet exemple, en cas d'erreur de saisie vous aurez droit à un message d'erreur un peu plus clair que ceux distillés par Windows. Pour le vérifier, tapez votre chiffre en vous trompant dans le séparateur décimal (le point au lieu de la virgule et vice versa). Sans la gestion d'erreur, vous saurez seulement que votre saisie n'est pas valide sans comprendre pourquoi, car vous avez bien entré un nombre alors que grâce à la gestion des exceptions, vous aurez le droit à :

Figure 1Exemple Try..Except

En plus, vous pouvez ajouter des instructions remettant votre programme dans un état stable (réinitialisation de variable par exemple).

L'exemple ci-dessous est une des façons d'écrire la gestion des exceptions par try..except. Dans ce cas précis, nous savions ce qui pouvait provoquer une erreur dans le code protégé (une erreur de conversion) et nous n'avons traité que ce cas.

D'une manière plus générale, on peut considérer que la gestion d'exception peut intercepter des erreurs prévisibles et d'autres, plus aléatoires (non prévues) et que l'on peut soit traiter les erreurs prévisibles soit les autres ou les deux (ce qui est quand même préférable).

Quand on veut traiter une erreur prévisible, il faut savoir à quelle classe elle appartient, par exemple une erreur de conversion appartient à la classe EConvertError (on peut savoir ceci en consultant dans l'aide, c'est le type d'erreur soulevée par une fonction particulière).

Pour les erreurs imprévues, on peut utiliser un simple else, qui exécutera une suite d'instructions par défaut. Par exemple afficher un message d'erreur pour l'utilisateur avec des causes possibles de la raison.

Le try..except pourra se présenter ainsi :

try
{instructions}
except
{instructions éventuelles communes à tous les cas possibles d'erreur}
// Gestion des cas prévisibles d'erreur
on Exception1 do InstructionTraitantErr1;
on Exception2 do InstructionTraitantErr2;
....
on Exception(n) do InstructionTraitantErr(n);
else
InstructionTraitantLesCasNonPrevue;
end;

Un exemple complet :
Rappel sur l'utilisation du finally. Son utilisation permet de s'assurer qu'une suite d'instructions va être réalisée même en cas d'erreur dans le code situé avant. Mais l'erreur doit être traitée par un couple try..except.

Pour vous aider à bien comprendre, je vous propose l'exemple concret ci-dessous.

C'est un extrait du cours sur TFileStream, mais pour éviter d'aller voir, ce qui est un tort :) , voici une petite explication. En gros la fonction ouvre un fichier et place le contenu dans une variable de type record (mrec). Pour que les choses se passent bien, le fichier doit exister, on doit pouvoir l'ouvrir, etc. De plus, lorsque le fichier est ouvert par notre application (les autres aussi), il est indisponible pour d'autres applications. Il nous faut donc nous assurer qu'on le fermera dès qu'il ne sera plus nécessaire. Grâce au couple try..except et try..finally, nous faisons tout ça.

Exemple du traitement de l'erreur et de l'utilisation du finally

function OuvrirFichier(Nom : TFileName;var mrec : TMonRecord) : boolean;
var
F : TFileStream;
i : integer;
begin
// Ouvre un fichier et affecte le contenu à un record
F := nil;
try // Ce try est lié à l'except. Par son intermédiaire, on va traiter les erreurs
// Ouverture d'un flux sur le fichier en lecture.
// Le fichier reste accessible en lecture seule pour d'autres appli.
F := TFileStream.Create(Nom, fmOpenRead or fmShareDenyWrite);
try // ce try-ci est lié au finally. Ici on veut s'assurer que le fichier soit libéré quoiqu'il advienne.
// Vous remarquez que la demande de création est extérieure au try..finally, n'est ce pas ?
// En effet, il n'y a rien à libérer si cela n'a pas été créé. Le try..except gérera ce cas là.
// En plus, try..except géra une erreur éventuelle survenue entre try..finally.

// Je vous laisse le code de lecture notamment pour que vous réalisez que ce code pourrait planter.
// Si vous regardez bien, vous verrez plusieurs raisons possibles :
// Le nombre de champs peut ne pas être celui attendu,
// Des erreurs de conversions peuvent survenir, etc.
F.Position := 0; // Début du flux
if (pos('.zio',ExtractFileName(Nom))>0) then // Vérification du type du Fichier
begin
// Lecture du fichier
while (F.position < F.Size) do // Tant que la fin du fichier n'est pas atteinte faire :
begin
with mrec do // voir TMonRecord pour savoir quel type de donnée nous avons.
begin
// Lit la valeur et déplace la position en cours
// La première valeur lue est un entier (le fichier a été enregistré ainsi)
F.ReadBuffer(entier, SizeOf(integer));
// Ensuite nous avons un réel
F.ReadBuffer(reel, SizeOf(Double));
// De nouveau un entier, mais sa valeur n'est pas directement exploitable dans mrec.
// On le convertit, ici il s'agit d'un type énuméré.
F.ReadBuffer(i, SizeOf(integer));
enum := TEnumTest(i);
// On lit ensuite une Chaine de caractères dont la taille est limitée (string[taille]).
F.ReadBuffer(chainelimit, SizeOf(Chainelimit));
// On lit un entier correspondant à la taille de la chaine qui suit
F.ReadBuffer(i, SizeOf(i));
// Allocation d'assez d'espace dans la chaine pour la lecture
SetLength(chaine, i);
// Lecture de la chaine, on transmet un pointeur sur la chaine soit Chaine[1].
F.ReadBuffer(chaine[1], i);
end;
end;
Result := true;
end
else
begin
MessageDlg('Erreur ce n''est pas un fichier test.', mtError, [mbOk], 0);
Result := false;
end;
finally
// Ici, on libère le fichier. Comme la libération est dans la partie finally,
// on est sûr que le fichier sera bien libéré.
F.Free;
end; // fin try..finally
except
// Là, on traite les erreurs.
Result := False;
on EInOutError do
begin
MessageDlg('Erreur d''E-S fichier.', mtError, [mbOk], 0);
end;
on EReadError do
begin
MessageDlg('Erreur de lecture sur le fichier.', mtError, [mbOk], 0);
end;
else
begin
// Ici, on traite les erreurs auxquelles on n'a pas pensé ou dont on ne veut pas s'encombrer
// par exemple, les erreurs de conversion.
MessageDlg('Erreur sur le fichier.', mtError, [mbOk], 0);
end;
end; // fin try..except
// Les erreurs ont été traitées, le programme continuera donc normalement. D'autant plus, que nous renvoyons
// le résultat de l'opération (result)
end;

Précision

La protection d'un bloc de code par try..except permet d'éviter la propagation du message d'erreur, mais dans certains cas, il peut être nécessaire de relancer sa diffusion. La commande raise peut être utilisée à cet effet voir son chapitre pour plus de précision.

III-B - Liste non exhaustive des classes d'exception

Ceci est une liste incomplète des classes d'exception que vous pouvez être amené à utiliser.

- EconvertError : erreur de conversion, vous essayez de convertir un type en un autre alors qu'ils sont incompatibles.
Exemple : un string en integer (StrToInt) avec un string ne correspondant pas à un nombre entier.

- EDivByZero : le programme a tenté de faire une division par zéro (pas cool).

- EFOpenError : le programme ne peut ouvrir un fichier spécifié (le fichier n'existe pas par exemple).

- EInOutError : erreur d'entrée-sortie, sur le fichier spécifié.

- EReadError : le programme tente de lire des données dans un flux, mais ne peut lire le nombre spécifié d'octets.

- ERangeError : débordement de taille. Le programme dépasse les bornes d'un type entier ou les limites d'un tableau.

- EAbort : exception spéciale, car elle n'affiche pas de message d'erreur. On peut s'en servir pour annuler une tâche en cours si une condition arrive (une erreur par exemple). Pour la déclencher un simple appel à Abort; suffit, une exception EAbort est alors générée. C'est un moyen simple de créer une exception personnalisée, il suffit alors de traiter l'exception on EAbort do.

Si vous n'arrivez pas à trouver la classe d'exception correspondant à votre code, tenter de provoquer l'erreur. Dans le message d'erreur, Delphi vous indiquera la classe d'exception (si elle existe), ce message permet aussi de tester si on a intercepté la bonne classe d'exception.

IV - Exception personnalisée

Toutes les exceptions dérivent de la Class exception, vous pouvez donc créer vos propres classes d'exception en héritant de cette classe. Ceci peut vous permettre de gérer votre programme par exception en supprimant tous les tests des cas qui ne vous intéressent pas (exemple : la vérification qu'un diviseur est non nul).

En disant que toutes les exceptions dérivent de la classe Exception. Ce qui n'est pas tout à fait exact. En fait n'importe quel objet peut être déclenché en tant qu'exception. Cependant, les gestionnaires d'exceptions standards ne gèrent que les exceptions dérivant de la classe exception.

Dans votre programme, si vous voulez déclarer une nouvelle classe d'exception, vous aurez à entrer le code suivant :

type
MonException = class(Exception)
HelpContext : THelpContext; // Contexte dans l'aide
Message : string; // Message d'erreur
public
procedure FonctionGerantErreur(); // Fonction à appeler en cas d'erreur
end;
procedure FonctionGerantErreur();
begin
{instructions}
end;

Seule la première ligne est obligatoire. Si vous ne précisez pas le reste, la seule information disponible lors du déclenchement de votre exception sera son nom. FonctionGerantErreur est le nouveau gestionnaire de l'exception, dans cette fonction vous mettrez le code assurant la stabilité de votre application.

Pour accéder au message ou à la Méthode d'une Exception (FonctionGerantErreur) tapez E.Message ou E.MaFonction. Dans ce cas le try..except doit s'écrire ainsi.

try
{instructions}
except
on E : Exception do
ShowMessage('Message : ' + E.Message);
// Et/Ou
E.MaFonction; // Permet de centraliser le code gérant un type d'erreur
end;

Exemple : Exception personnalisée

type
EValeurIncorrect = class(Exception);

et dans le code

if Valeur <> ValeurCorrect then
raise EValeurIncorrect.Create('Valeur ne fait pas partie des valeurs autorisées');

La propriété Message de la classe Exception (y compris les classes dérivées) et l'affichage de message personnalisé dans le bloc except/end sont équivalents. Pour les classes d'exception déjà existantes, on préférera sans doute rendre le message plus explicite tandis que pour les classes personnalisées on aura recours à la propriété Message plutôt que d'indiquer à chaque fois le texte.

Il en est de même pour les instructions gérant l'erreur. On n'utilisera la Méthode de la classe que pour ceux personnalisés, évitant d'utiliser une fonction orpheline ou pire de réécrire à chaque fois le code.

Reportez-vous sur les classes pour plus de renseignements sur l'héritage.

V - Raise

Protéger ainsi votre code, vous permet d'intercepter les messages d'erreur. Toutefois dans certaines situations, vous souhaiterez que le message d'erreur soit propagé pour qu'il soit intercepté par une autre gestion des exceptions.

Prenons l'exemple de l'assignation de fichier, plutôt que d'utiliser une variable de retour pour indiquer le résultat de l'opération, on pourrait transmettre le message d'erreur éventuel. À cet effet, la commande raise est à votre disposition.

Raise ne sert pas uniquement à propager un message d'erreur, on peut aussi s'en servir pour déclencher une exception (en général pour déclencher une exception personnalisée comme vous avez pu le voir plus haut).

Exemple de déclenchement d'exception personnalisée.

if Valeur <> ValeurCorrect then
raise EValeurIncorrect.Create('Valeur ne fait pas partie des valeurs autorisées');

Exemple complet de l'utilisation de raise

unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;
MonException = class(Exception)
public
function GestErr():string;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function MonException.GestErr():string;
begin
if MessageDlg('La variable transmise est incorrecte continuer avec la valeur par défaut', mtInformation, [mbYes, mbNo], 0) =
mrYes then
begin
Result := 'très petit';
end;
end;
function DoQuelqueChose(valeur : double):string;
begin
// La fonction ne travaille que sur des nombres réels positifs
if valeur < 0 then
begin
raise MonException.Create('Erreur: Travaille impossible !');
end;
// Travaille sur valeur complètement sans intérêt
if valeur < 10 then
result := 'très petit'
else if valeur <= 50 then
result := 'moitié de cent'
else if valeur <= 100 then
result := 'égal à cent'
else
result := 'très grand';
end;
procedure TForm1.Button1Click(Sender: TObject);
var
r : double;
tmpstr : string;
begin
try
r := StrToFloat(Edit1.Text);
tmpstr := DoQuelqueChose(r);
except
on E : MonException do
tmpstr := E.GestErr;
on EconvertError do
begin
ShowMessage('Erreur de Saisie : nombre attendu'
+#10#13+'Séparteur décimal : '+DecimalSeparator);
tmpstr := 'Mauvaise saisie';
end;  // Attention n'oubliez pas ce point-virgule (on n’est pas dans le cas if/else !)
else
begin
ShowMessage('Erreur Inconnue');
tmpstr := 'Invalid';
end;
end;
ShowMessage('Résultat : '+tmpStr);
end;
end;

VI - Conclusion

Grâce aux exceptions apparues avec la programmation objet, le développeur a maintenant à sa disposition un outil efficace pour protéger son programme des aléas de l'informatique. J'espère que le présent tutoriel a été pour vous une mine d'informations et que désormais vous aborderez la gestion des exceptions avec sérénité.