Tous les articles

Business Central

Gestion des erreurs dans une extension AL Business Central : ce que font deux bases de code réelles

AL offre quatre façons de traiter une erreur, et aucune n'est une question d'élégance. Voici ce que font réellement deux de nos bases de code AL avec les labels, Error(''), les erreurs collectées, les try functions et Codeunit.Run, ce que l'utilisateur finit par voir, et où sont les points faibles.

Un panneau de contrôle bleu nuit fait de petites tuiles plates sur fond de studio gris clair ; quatre tuiles sont soulevées et cerclées de turquoise, reliées par de fins traits à un petit plateau qui aligne quatre points ambre — des erreurs rassemblées, pas éparpillées.

Gestion des erreurs dans une extension AL Business Central : ce que font deux bases de code réelles

AL offre quatre façons de traiter une erreur : la lever avec un label, en collecter plusieurs avant de s’arrêter, la rattraper dans une try function, ou l’isoler derrière Codeunit.Run. Le choix n’est jamais une affaire d’élégance. Il porte sur deux choses avec lesquelles l’utilisateur vivra ensuite : ce qu’il voit à l’écran, et ce que la base de données conserve quand le code s’arrête. Ratez l’une des deux et l’extension paraît impeccable en démo et ment en production.

Voici ce que font réellement deux de nos bases de code AL. La première est un produit métier de 972 fichiers AL, 174 codeunits et 316 appels actifs à Error. La seconde est notre app AppSource de facturation électronique : 118 fichiers de production, 61 codeunits de test, 576 tests.

Pourquoi un message d’erreur AL ne doit-il jamais être une chaîne en dur ?

Parce qu’une chaîne qu’on ne retrouve pas est une chaîne qu’on ne traduit pas, qu’on ne teste pas et qu’on ne trace pas. Sur les 316 appels actifs à Error du produit, 60 passent une chaîne vide et aucun ne passe de texte littéral ; le seul littéral du dépôt dort dans une ligne en commentaire. Tous les autres passent une variable Label ou un ErrorInfo. L’app de facturation électronique obtient le même score sur ses 54 appels actifs de production : pas un seul littéral.

Ce n’est pas de la discipline, c’est un ruleset. La configuration des analyseurs des deux dépôts signale un Error avec du texte brut, et nous gardons cette règle active parce que la télémétrie a besoin d’un identifiant stable, pas d’une phrase qui change à chaque relecture. Les labels d’erreur portent le suffixe Err, et c’est la plus grosse catégorie de labels du produit, 271 sur 697.

Le bénéfice se voit dans les tests. Quand la production lève un label, le test vérifie le même symbole et ne casse que si la logique change, pas la formulation. Il survit aussi à un bac à sable français : un test qui comparait un fragment anglais d’un label traduit restait vert en CI, qui tourne en en-US, et échouait en fr-FR, cette assertion fixe donc désormais la langue de session.

À quoi sert vraiment Error(‘’) ?

À annuler, pas à échouer. La documentation Microsoft le dit sans détour : appeler Error avec une chaîne vide termine l’exécution du code AL sans afficher de message. La transaction est annulée et l’utilisateur ne voit rien.

Sur les 60 appels à chaîne vide du produit, la forme dominante tient en une ligne après une boîte de confirmation : si l’utilisateur répond Non, appeler Error(‘’). La boîte de dialogue était le message ; l’erreur vide est le retour en arrière. Cette base compte 61 appels à Confirm Management et 60 erreurs vides, et les deux nombres se suivent précisément pour cette raison. Un deuxième usage désactive la recherche et le drill-down sur un champ, un non-événement délibéré que l’utilisateur ne peut pas forcer.

La règle qui va avec : une erreur vide est silencieuse par construction, donc elle n’est jamais la bonne réponse à un vrai échec. Si quelque chose a mal tourné, quelqu’un doit pouvoir savoir quoi.

Comment fonctionnent les erreurs collectées en AL, et quand sont-elles rentables ?

Elles sont rentables chaque fois qu’un utilisateur corrigerait un champ, cliquerait de nouveau, et tomberait sur l’erreur suivante. Depuis la version 8.0 du runtime, une procédure décorée de l’attribut ErrorBehavior Collect continue après une erreur collectable et la met de côté ; à la fin, on interroge HasCollectedErrors et GetCollectedErrors et on décide quoi montrer. Un détail : ErrorInfo.Create() sans argument crée l’erreur comme collectable, la propriété n’a donc jamais à être posée à la main.

Le produit s’en sert dans un gabarit répété pour chacune de ses entités principales. Une codeunit de contrôle de complétude vide les erreurs collectées à l’entrée, déroule chaque contrôle jusqu’au bout, et stocke la liste. Chaque contrôle lève un ErrorInfo qui porte le RecordId et le FieldNo concernés, ce qui permet au message d’atterrir sur une ligne et une colonne plutôt que dans une boîte générique. Plusieurs tables exposent une procédure IsReady qui renvoie un booléen, et quatre d’entre elles offrent aussi une surcharge qui remplit un tampon Error Message temporaire, de sorte que les appelants décident eux-mêmes d’afficher, d’interrompre, ou de marquer l’enregistrement et de continuer.

Deux choix de conception méritent d’être copiés. Premièrement, les contrôles qui réutilisent la validation de la plateforme enveloppent un simple TestField dans une try function, puis relèvent GetLastErrorText dans la collection. Le message obtenu est celui que Business Central a déjà écrit, localisé et légendé avec le nom du champ. Deuxièmement, la liste d’ErrorInfo collectés est convertie dans la table standard Error Message via LogDetailedMessage, pour que la page Messages d’erreur ordinaire l’affiche. Deux modèles d’erreur, une passerelle, aucune page sur mesure.

Une mise en garde vient directement de la documentation Microsoft : vider la liste collectée n’annule pas les écritures en base, d’où le couplage documenté, collecter à l’intérieur d’un bloc if Codeunit.Run.

Quand une try function est-elle le mauvais outil ?

Chaque fois qu’elle cache un échec, et chaque fois qu’elle écrit. Les modifications faites en base à l’intérieur d’une try function ne sont pas annulées quand l’erreur survient. Business Central en ligne n’impose aucune restriction sur ces écritures ; on-premises les bloque par défaut. Dans les propres exécutions de tests de l’app de facturation électronique, une écriture dans une try function a échoué deux fois, le dépôt la traite donc comme interdite partout et le dit dans le code.

Le produit compte 30 try functions et 126 appels à GetLastErrorText. Le ratio de quatre pour un vient de trois enveloppes génériques, TryGet, TryValidate et TryFindFirst, qui acceptent n’importe quel enregistrement en Variant et sont appelées depuis 84 endroits. C’est l’idiome : une try function existe pour que le lot continue, et chaque branche d’échec finit dans une erreur journalisée, jamais dans le vide.

L’app de facturation électronique pousse la même règle jusqu’à sa limite. Sa codeunit de transport HTTP est documentée ainsi : « rien ici ne peut lever d’erreur ». Une URL vide ou un en-tête illégal sont capturés par le retour booléen de SetRequestUri et de Send, et signalés par un statut 0, parce qu’une erreur levée aussi bas traverserait toutes les enveloppes try et interromprait l’entrée de file d’attente des travaux entière. Les try functions qui protègent des étapes non critiques, comme l’enregistrement d’une approbation après validation comptable, finissent chacune dans une ligne de télémétrie qui embarque GetLastErrorText, avec un commentaire qui dit pourquoi : une erreur non gérée à cet endroit annulerait la validation.

La seule inversion dans le produit est instructive : une intégration avec un service externe portait autrefois le bloc complet de collecte et de journalisation, aujourd’hui en commentaire et remplacé par un simple exit(false). L’appelant apprend que le contrôle a échoué et rien sur la raison. Un booléen qui était autrefois une explication.

Quand isoler un échec derrière Codeunit.Run ?

Quand l’échec ne doit pas emporter le reste du travail avec lui, et quand c’est un retour en arrière que l’on veut. Avec sa valeur de retour utilisée, Codeunit.Run valide les modifications de la codeunit à la fin sauf si une erreur survient, et si l’on est déjà dans une transaction il faut valider avant. Il isole donc avec retour en arrière, ce qu’une try function ne fait pas.

L’app de facturation électronique exécute chacune de ses migrations de données dans son propre Codeunit.Run. Quand l’une échoue, le texte de l’échec part en télémétrie via GetLastErrorText, avec la verbosité Error et la portée la plus large, et la migration suivante s’exécute quand même. Le commentaire dit pourquoi elle ne lève pas d’erreur à la place : lever, c’était le défaut qu’on corrigeait, et ici cela interromprait aussi l’autre migration.

Le produit fait délibérément l’inverse. Cinq appels à Codeunit.Run sur 972 fichiers, et 135 Commit explicites, presque tous avec un commentaire de justification parce que le ruleset avertit sur un Commit nu. Les raisons qui reviennent : valider après chaque message pour qu’un message empoisonné n’annule pas ceux qui l’ont précédé, et valider tous les mille enregistrements pour éviter les transactions longues. Un message en échec est marqué traité et ignoré, validé, et laissé à une page de rejeu. L’échec partiel est une politique écrite, pas un accident.

Que voit vraiment l’utilisateur quand une entrée de file d’attente échoue ?

Ce que vous avez décidé de conserver. Quand l’objet exécuté par une entrée de file d’attente des travaux lève une erreur, le gestionnaire d’erreurs de la file passe l’entrée au statut Erreur, enregistre les erreurs via Error Message Management, puis s’arrête ou replanifie selon le nombre maximal de tentatives et le délai de reprise. L’utilisateur dispose d’une action Afficher l’erreur sur l’entrée.

Le produit ne conçoit rien de plus. Il lit le journal de la file d’attente de la plateforme via des FlowFields sur sa table de paramétrage, expose les horodatages du dernier succès et de la dernière erreur, et son action Afficher l’erreur délègue au ShowErrorMessage de l’entrée elle-même. Zéro stockage dupliqué.

L’app de facturation électronique ne peut pas se permettre ce minimalisme, parce que ses échecs se produisent face à une plateforme externe. Elle tient donc un journal des échanges API : une ligne par appel, avec la méthode HTTP, le code de statut réellement renvoyé par la plateforme, un indicateur Successful, et les deux corps. Un test vérifie qu’une récupération en échec laisse une ligne avec le statut 422 et Successful à faux, et le message d’assertion dit pourquoi : ce statut est ce qui distingue un 503 passager d’un 422 permanent qu’il faut aller chercher auprès de la plateforme.

Une limite honnête est écrite dans les tests eux-mêmes. La télémétrie émise par Session.LogMessage n’a aucune surface observable en AL : pas d’événement, pas de journal injectable, rien qu’un test puisse relire. L’app compte 17 appels de télémétrie en production, et les fichiers de test disent en toutes lettres que ces traces sont garanties par la revue de code, pas par les tests.

Comment teste-t-on les chemins d’erreur en AL ?

Avec asserterror, contre le label, et parfois sans asserterror du tout. L’app de facturation électronique compte 49 instructions asserterror et 37 assertions ExpectedError sur 576 tests. Quatre tests portent un commentaire qui dit « pas d’asserterror, volontairement » : le comportement testé est qu’un appel revient les mains vides au lieu de lever une erreur, parce qu’une erreur non gérée à cet endroit interromprait l’entrée de file d’attente entière, et qu’un test qui échoue sur une levée est précisément le symptôme de production.

Le produit est le contre-exemple, et nous le disons : 17 méthodes de test, 2 asserterror actifs, et trois autres mis en commentaire avec leurs assertions laissées en place. Une base de 174 codeunits, 316 points d’erreur et deux tests de chemin d’erreur a son point faible exactement là.

L’approche Asio Services

La gestion des erreurs est un contrat avec la personne qui lira le message à dix-huit heures un jour de clôture. Le nôtre a quatre clauses : chaque message est un label, chaque sortie silencieuse est une annulation, chaque try function finit dans une trace, et chaque lot décide par écrit ce qui survit à un échec. C’est le même raisonnement qui permet de maintenir une grosse extension AL à jour deux fois par an, et une partie de la raison pour laquelle certaines des extensions AL à connaître existent pour produire de meilleures erreurs que celles qu’on écrirait à la main.

Si votre extension échoue en silence dans une file d’attente, ou si vos utilisateurs rencontrent les erreurs un champ à la fois, c’est du développement Business Central au périmètre clair. Commencez par notre formulaire de clarté et nous vous dirons laquelle des quatre clauses manque.

FAQ

Quelle est la différence entre une try function et Codeunit.Run en AL ?

Une try function rattrape l’erreur et renvoie faux, mais les modifications faites en base à l’intérieur ne sont pas annulées. Codeunit.Run, avec sa valeur de retour utilisée, renvoie aussi faux en cas d’erreur et annule les modifications de la codeunit ; il valide quand aucune erreur ne survient, et exige un Commit préalable si l’on est déjà dans une transaction.

Que fait Error(‘’) dans Business Central ?

Il termine l’exécution et annule la transaction sans afficher le moindre message. C’est la façon idiomatique d’annuler après qu’un utilisateur a répondu Non à une confirmation. Il ne doit jamais servir à signaler un vrai échec, parce que personne ne pourra savoir ce qui s’est passé.

Que sont les erreurs collectées dans Business Central ?

Une procédure portant l’attribut ErrorBehavior Collect continue après les erreurs collectables et les rassemble, pour montrer à l’utilisateur tous les problèmes d’un coup. ErrorInfo.Create() crée une erreur collectable par défaut. La fonctionnalité existe depuis la version 8.0 du runtime.

Votre Business Central est-il le problème, ou le symptôme ?

On audite ce que vous faites tourner réellement, on nomme ce qui vaut le coup d’être gardé, et on tue le reste. Une conversation suffit en général à savoir dans quel cas vous êtes.

Commence par la clarté