Peu importe à quel point nous sommes doués en programmation, nos scripts contiennent parfois des erreurs. Ils peuvent survenir à cause de nos erreurs, d’une entrée inattendue de l’utilisateur, d’une réponse erronée du serveur et pour mille autres raisons.
Habituellement, un script « meurt » (s'arrête immédiatement) en cas d'erreur, l'imprimant sur la console.
Mais il existe une construction syntaxique try...catch
qui nous permet de « détecter » les erreurs afin que le script puisse, au lieu de mourir, faire quelque chose de plus raisonnable.
La construction try...catch
comporte deux blocs principaux : try
, puis catch
:
essayer { //code... } attraper (erreur) { // gestion des erreurs }
Cela fonctionne comme ceci :
Tout d’abord, le code de try {...}
est exécuté.
S'il n'y a eu aucune erreur, alors catch (err)
est ignoré : l'exécution atteint la fin de try
et continue en sautant catch
.
Si une erreur se produit, l'exécution try
est arrêtée et le contrôle passe au début de catch (err)
. La variable err
(nous pouvons lui donner n'importe quel nom) contiendra un objet d'erreur avec des détails sur ce qui s'est passé.
Ainsi, une erreur à l’intérieur du bloc try {...}
ne tue pas le script – nous avons une chance de la gérer dans catch
.
Regardons quelques exemples.
Un exemple sans erreur : affiche alert
(1)
et (2)
:
essayer { alert('Début des essais'); // (1) <-- // ...aucune erreur ici alert('Fin des essais'); // (2) <-- } attraper (erreur) { alert('Catch est ignoré car il n'y a pas d'erreur'); // (3) }
Un exemple avec une erreur : montre (1)
et (3)
:
essayer { alert('Début des essais'); // (1) <-- lalala; // erreur, la variable n'est pas définie ! alert('Fin de l'essai (jamais atteint)'); // (2) } attraper (erreur) { alert(`Une erreur s'est produite !`); // (3) <-- }
try...catch
ne fonctionne que pour les erreurs d'exécution
Pour que try...catch
fonctionne, le code doit être exécutable. En d’autres termes, il doit s’agir d’un JavaScript valide.
Cela ne fonctionnera pas si le code est syntaxiquement incorrect, par exemple s'il contient des accolades inégalées :
essayer { {{{{{{{{{{{{{ } attraper (erreur) { alert("Le moteur ne comprend pas ce code, il n'est pas valide"); }
Le moteur JavaScript lit d'abord le code, puis l'exécute. Les erreurs qui se produisent lors de la phase de lecture sont appelées erreurs de « temps d'analyse » et sont irrécupérables (depuis l'intérieur de ce code). C'est parce que le moteur ne peut pas comprendre le code.
Donc, try...catch
ne peut gérer que les erreurs qui se produisent dans un code valide. De telles erreurs sont appelées « erreurs d’exécution » ou, parfois, « exceptions ».
try...catch
fonctionne de manière synchrone
Si une exception se produit dans le code « planifié », comme dans setTimeout
, alors try...catch
ne l'attrapera pas :
essayer { setTimeout(fonction() { noSuchVariable ; // le script va mourir ici }, 1000); } attraper (erreur) { alert( "ne fonctionnera pas" ); }
C'est parce que la fonction elle-même est exécutée plus tard, lorsque le moteur a déjà quitté la construction try...catch
.
Pour intercepter une exception dans une fonction planifiée, try...catch
doit être à l'intérieur de cette fonction :
setTimeout(fonction() { essayer { noSuchVariable ; // try...catch gère l'erreur ! } attraper { alert( "une erreur est détectée ici!" ); } }, 1000);
Lorsqu'une erreur se produit, JavaScript génère un objet contenant les détails la concernant. L'objet est ensuite passé en argument à catch
:
essayer { //... } catch (err) { // <-- "l'objet erreur", pourrait utiliser un autre mot au lieu de err //... }
Pour toutes les erreurs intégrées, l'objet d'erreur a deux propriétés principales :
name
Nom de l'erreur. Par exemple, pour une variable non définie, c'est "ReferenceError"
.
message
Message textuel sur les détails de l'erreur.
Il existe d'autres propriétés non standard disponibles dans la plupart des environnements. L’un des plus largement utilisés et pris en charge est :
stack
Pile d'appels actuelle : une chaîne contenant des informations sur la séquence d'appels imbriqués qui ont conduit à l'erreur. Utilisé à des fins de débogage.
Par exemple:
essayer { lalala; // erreur, la variable n'est pas définie ! } attraper (erreur) { alert(err.name); // Erreur de référence alert(err.message); // lalala n'est pas défini alert(err.stack); // ReferenceError : lalala n'est pas défini à (...pile d'appels) // Peut également afficher une erreur dans son ensemble // L'erreur est convertie en chaîne sous la forme "nom : message" alerte (erreur); // ReferenceError : lalala n'est pas défini }
Un ajout récent
Il s'agit d'un ajout récent à la langue. Les anciens navigateurs peuvent avoir besoin de polyfills.
Si nous n'avons pas besoin des détails de l'erreur, catch
peut les omettre :
essayer { //... } catch { // <-- sans (erreur) //... }
Explorons un cas d'utilisation réel de try...catch
.
Comme nous le savons déjà, JavaScript prend en charge la méthode JSON.parse(str) pour lire les valeurs codées en JSON.
Habituellement, il est utilisé pour décoder les données reçues sur le réseau, depuis le serveur ou une autre source.
Nous le recevons et appelons JSON.parse
comme ceci :
let json = '{"name": "John", "age": 30}'; // données du serveur laissez l'utilisateur = JSON.parse(json); // convertit la représentation textuelle en objet JS // maintenant l'utilisateur est un objet avec les propriétés de la chaîne alert( utilisateur.nom ); // John alert( utilisateur.age ); // 30
Vous pouvez trouver des informations plus détaillées sur JSON dans les méthodes JSON, au chapitre JSON.
Si json
est mal formé, JSON.parse
génère une erreur, donc le script « meurt ».
Faut-il s'en contenter ? Bien sûr que non!
De cette façon, si quelque chose ne va pas avec les données, le visiteur ne le saura jamais (à moins qu'il n'ouvre la console du développeur). Et les gens n'aiment vraiment pas quand quelque chose « meurt » sans aucun message d'erreur.
Utilisons try...catch
pour gérer l'erreur :
let json = "{ mauvais json }" ; essayer { laissez l'utilisateur = JSON.parse(json); // <-- lorsqu'une erreur se produit... alert( utilisateur.nom ); // ne marche pas } attraper (erreur) { // ...l'exécution saute ici alert( "Nos excuses, les données comportent des erreurs, nous essaierons de les demander une nouvelle fois." ); alert( err.name ); alert( err.message ); }
Ici, nous utilisons le bloc catch
uniquement pour afficher le message, mais nous pouvons faire bien plus : envoyer une nouvelle requête réseau, suggérer une alternative au visiteur, envoyer des informations sur l'erreur à un outil de journalisation,… . Tout cela est bien mieux que de simplement mourir.
Que se passe-t-il si json
est syntaxiquement correct, mais n'a pas de propriété name
obligatoire ?
Comme ça:
let json = '{ "age": 30 }'; // données incomplètes essayer { laissez l'utilisateur = JSON.parse(json); // <-- aucune erreur alert( utilisateur.nom ); // pas de nom ! } attraper (erreur) { alert( "ne s'exécute pas" ); }
Ici, JSON.parse
s'exécute normalement, mais l'absence de name
est en fait une erreur pour nous.
Pour unifier la gestion des erreurs, nous utiliserons l’opérateur throw
.
L'opérateur throw
génère une erreur.
La syntaxe est :
lancer <objet d'erreur>
Techniquement, nous pouvons utiliser n'importe quoi comme objet d'erreur. Cela peut même être une primitive, comme un nombre ou une chaîne, mais il est préférable d'utiliser des objets, de préférence avec des propriétés name
et message
(pour rester quelque peu compatible avec les erreurs intégrées).
JavaScript possède de nombreux constructeurs intégrés pour les erreurs standard : Error
, SyntaxError
, ReferenceError
, TypeError
et autres. Nous pouvons également les utiliser pour créer des objets d’erreur.
Leur syntaxe est :
let error = new Error(message); // ou let error = new SyntaxError(message); let error = new ReferenceError(message); //...
Pour les erreurs intégrées (pas pour les objets, juste pour les erreurs), la propriété name
est exactement le nom du constructeur. Et message
est tiré de l’argument.
Par exemple:
let error = new Error("Des choses arrivent o_O"); alert(erreur.nom); // Erreur alert(erreur.message); // Des choses arrivent o_O
Voyons quel type d'erreur JSON.parse
génère :
essayer { JSON.parse("{ mauvais json o_O }"); } attraper (erreur) { alert(err.name); // Erreur de syntaxe alert(err.message); // Jeton inattendu b en JSON en position 2 }
Comme nous pouvons le voir, c'est une SyntaxError
.
Et dans notre cas, l’absence de name
est une erreur, car les utilisateurs doivent avoir un name
.
Alors lançons-le :
let json = '{ "age": 30 }'; // données incomplètes essayer { laissez l'utilisateur = JSON.parse(json); // <-- aucune erreur si (!user.name) { throw new SyntaxError("Données incomplètes : pas de nom"); // (*) } alert( utilisateur.nom ); } attraper (erreur) { alert( "Erreur JSON : " + err.message ); // Erreur JSON : Données incomplètes : pas de nom }
Dans la ligne (*)
, l'opérateur throw
génère une SyntaxError
avec le message
donné, de la même manière que JavaScript le générerait lui-même. L'exécution de try
s'arrête immédiatement et le flux de contrôle passe à catch
.
Désormais, catch
est devenu un endroit unique pour toute la gestion des erreurs : à la fois pour JSON.parse
et dans d'autres cas.
Dans l'exemple ci-dessus, nous utilisons try...catch
pour gérer les données incorrectes. Mais est-il possible qu'une autre erreur inattendue se produise dans le bloc try {...}
? Comme une erreur de programmation (la variable n'est pas définie) ou autre chose, pas seulement cette question de « données incorrectes ».
Par exemple:
let json = '{ "age": 30 }'; // données incomplètes essayer { utilisateur = JSON.parse(json); // <-- j'ai oublié de mettre "let" avant l'utilisateur //... } attraper (erreur) { alert("Erreur JSON : " + erreur); // Erreur JSON : ReferenceError : l'utilisateur n'est pas défini // (pas d'erreur JSON en fait) }
Bien sûr, tout est possible ! Les programmeurs font des erreurs. Même dans les utilitaires open source utilisés par des millions de personnes depuis des décennies, un bug peut soudainement être découvert, entraînant de terribles piratages.
Dans notre cas, try...catch
est placé pour détecter les erreurs de « données incorrectes ». Mais de par sa nature, catch
récupère toutes les erreurs de try
. Ici, il obtient une erreur inattendue, mais affiche toujours le même message "JSON Error"
. C'est faux et rend également le code plus difficile à déboguer.
Pour éviter de tels problèmes, nous pouvons utiliser la technique du « rethrowing ». La règle est simple :
Catch ne doit traiter que les erreurs qu'il connaît et « relancer » toutes les autres.
La technique du « relance » peut être expliquée plus en détail comme :
Catch obtient toutes les erreurs.
Dans le bloc catch (err) {...}
nous analysons l'objet d'erreur err
.
Si nous ne savons pas comment le gérer, nous throw err
.
Habituellement, nous pouvons vérifier le type d’erreur à l’aide de l’opérateur instanceof
:
essayer { utilisateur = { /*...*/ }; } attraper (erreur) { if (erreur instanceof ReferenceError) { alert('Erreur de référence'); // "ReferenceError" pour accéder à une variable non définie } }
Nous pouvons également obtenir le nom de la classe d’erreur à partir de la propriété err.name
. Toutes les erreurs natives l'ont. Une autre option consiste à lire err.constructor.name
.
Dans le code ci-dessous, nous utilisons le rethrowing pour que catch
ne gère que SyntaxError
:
let json = '{ "age": 30 }'; // données incomplètes essayer { laissez l'utilisateur = JSON.parse(json); si (!user.name) { throw new SyntaxError("Données incomplètes : pas de nom"); } blabla(); // erreur inattendue alert( utilisateur.nom ); } attraper (erreur) { if (erreur instanceof SyntaxError) { alert( "Erreur JSON : " + err.message ); } autre { jeter l'erreur; // relance (*) } }
L'erreur lancée en ligne (*)
depuis l'intérieur du bloc catch
"tombe" de try...catch
et peut être soit interceptée par une construction try...catch
externe (si elle existe), soit elle tue le script.
Ainsi, le bloc catch
ne gère en réalité que les erreurs qu’il sait gérer et « ignore » toutes les autres.
L'exemple ci-dessous montre comment de telles erreurs peuvent être détectées par un niveau supplémentaire de try...catch
:
fonction readData() { let json = '{ "age": 30 }'; essayer { //... blabla(); // erreur! } attraper (erreur) { //... if (!(err instanceof SyntaxError)) { jeter l'erreur; // relance (je ne sais pas comment gérer ça) } } } essayer { readData(); } attraper (erreur) { alert( "Le catch externe a obtenu : " + err ); // je l'ai attrapé ! }
Ici, readData
sait seulement comment gérer SyntaxError
, tandis que le try...catch
externe sait comment tout gérer.
Attendez, ce n'est pas tout.
La construction try...catch
peut avoir une clause de code supplémentaire : finally
.
S'il existe, il s'exécute dans tous les cas :
après try
, s'il n'y a pas eu d'erreur,
après catch
, s'il y a eu des erreurs.
La syntaxe étendue ressemble à ceci :
essayer { ... essayez d'exécuter le code ... } attraper (erreur) { ... gérer les erreurs ... } enfin { ... exécutez toujours ... }
Essayez d'exécuter ce code :
essayer { alert( 'essayer' ); if (confirm('Faire une erreur ?')) BAD_CODE(); } attraper (erreur) { alert( 'attraper' ); } enfin { alert( 'enfin' ); }
Le code a deux modes d'exécution :
Si vous répondez « Oui » à « Faire une erreur ? », alors try -> catch -> finally
.
Si vous dites « Non », try -> finally
.
La clause finally
est souvent utilisée lorsque nous commençons à faire quelque chose et que nous voulons le finaliser quel que soit le résultat.
Par exemple, nous voulons mesurer le temps que prend une fonction de nombres de Fibonacci fib(n)
. Naturellement, nous pouvons commencer à mesurer avant son exécution et terminer après. Mais que se passe-t-il s'il y a une erreur lors de l'appel de la fonction ? En particulier, l'implémentation de fib(n)
dans le code ci-dessous renvoie une erreur pour les nombres négatifs ou non entiers.
La clause finally
est un excellent endroit pour terminer les mesures quoi qu'il arrive.
Voici finally
la garantie que le temps sera mesuré correctement dans les deux situations – en cas d'exécution réussie de fib
et en cas d'erreur :
let num = +prompt("Entrer un nombre entier positif ?", 35) soit diff, résultat ; fonction fib(n) { si (n < 0 || Math.trunc(n) != n) { throw new Error("Ne doit pas être négatif, ni un entier."); } retourner n <= 1 ? n : fib(n - 1) + fib(n - 2); } let start = Date.now(); essayer { résultat = fib(num); } attraper (erreur) { résultat = 0 ; } enfin { diff = Date.now() - début ; } alert(result || "une erreur s'est produite"); alert( `l'exécution a pris ${diff}ms` );
Vous pouvez vérifier en exécutant le code en entrant 35
dans prompt
– il s'exécute normalement, finally
après try
. Et puis entrez -1
– il y aura une erreur immédiate et l'exécution prendra 0ms
. Les deux mesures sont effectuées correctement.
En d’autres termes, la fonction peut se terminer par return
ou throw
, cela n’a pas d’importance. La clause finally
s'exécute dans les deux cas.
Les variables sont locales à l'intérieur try...catch...finally
Veuillez noter que les variables result
et diff
dans le code ci-dessus sont déclarées avant try...catch
.
Sinon, si nous déclarions le bloc let
in try
, il ne serait visible qu'à l'intérieur de celui-ci.
finally
et return
La clause finally
fonctionne pour toute sortie de try...catch
. Cela inclut un return
explicite.
Dans l'exemple ci-dessous, il y a un return
dans try
. Dans ce cas, finally
est exécuté juste avant que le contrôle ne revienne au code externe.
fonction fonction() { essayer { renvoyer 1 ; } attraper (erreur) { /* ... */ } enfin { alert( 'enfin' ); } } alerte( func() ); // fonctionne d'abord avec l'alerte de Enfin, puis celle-ci
try...finally
La construction try...finally
, sans clause catch
, est également utile. Nous l'appliquons lorsque nous ne voulons pas gérer les erreurs ici (les laisser échouer), mais que nous voulons être sûrs que les processus que nous avons lancés sont finalisés.
fonction fonction() { // commence à faire quelque chose qui doit être complété (comme des mesures) essayer { //... } enfin { // termine ce truc même si tout le monde meurt } }
Dans le code ci-dessus, une erreur à l'intérieur try
apparaît toujours, car il n'y a pas catch
. Mais fonctionne finally
avant que le flux d'exécution ne quitte la fonction.
Spécifique à l'environnement
Les informations de cette section ne font pas partie du noyau JavaScript.
Imaginons que nous ayons une erreur fatale en dehors de try...catch
et que le script soit mort. Comme une erreur de programmation ou quelque autre chose terrible.
Existe-t-il un moyen de réagir à de tels événements ? Nous pouvons vouloir enregistrer l'erreur, montrer quelque chose à l'utilisateur (normalement, il ne voit pas les messages d'erreur), etc.
Il n'y en a pas dans la spécification, mais les environnements le fournissent généralement, car c'est vraiment utile. Par exemple, Node.js a process.on("uncaughtException")
pour cela. Et dans le navigateur, nous pouvons attribuer une fonction à la propriété spéciale window.onerror, qui s'exécutera en cas d'erreur non détectée.
La syntaxe :
window.onerror = function(message, url, ligne, col, erreur) { //... } ;
message
Message d'erreur.
url
URL du script où l'erreur s'est produite.
line
, col
Numéros de ligne et de colonne où l'erreur s'est produite.
error
Objet d'erreur.
Par exemple:
<script> window.onerror = function(message, url, ligne, col, erreur) { alert(`${message}n À ${line}:${col} de ${url}`); } ; fonction readData() { badFunc(); // Oups, quelque chose s'est mal passé ! } readData(); </script>
Le rôle du gestionnaire global window.onerror
n'est généralement pas de récupérer l'exécution du script – ce qui est probablement impossible en cas d'erreurs de programmation, mais d'envoyer le message d'erreur aux développeurs.
Il existe également des services Web qui fournissent une journalisation des erreurs dans de tels cas, comme https://errorception.com ou https://www.muscula.com.
Ils fonctionnent comme ceci :
Nous nous inscrivons au service et obtenons d'eux un morceau de JS (ou une URL de script) à insérer sur les pages.
Ce script JS définit une fonction window.onerror
personnalisée.
Lorsqu'une erreur se produit, il envoie une requête réseau à ce sujet au service.
Nous pouvons nous connecter à l'interface Web du service et voir les erreurs.
La construction try...catch
permet de gérer les erreurs d'exécution. Il permet littéralement « d’essayer » d’exécuter le code et de « détecter » les erreurs qui peuvent s’y produire.
La syntaxe est :
essayer { // exécute ce code } attraper (erreur) { // si une erreur se produit, sautez ici // err est l'objet d'erreur } enfin { // fait dans tous les cas après try/catch }
Il se peut qu'il n'y ait pas de section catch
ou no finally
, donc les constructions plus courtes try...catch
et try...finally
sont également valides.
Les objets d'erreur ont les propriétés suivantes :
message
– le message d’erreur lisible par l’homme.
name
– la chaîne avec le nom de l'erreur (nom du constructeur de l'erreur).
stack
(non standard, mais bien pris en charge) – la pile au moment de la création de l’erreur.
Si un objet d'erreur n'est pas nécessaire, nous pouvons l'omettre en utilisant catch {
au lieu de catch (err) {
.
Nous pouvons également générer nos propres erreurs en utilisant l'opérateur throw
. Techniquement, l'argument de throw
peut être n'importe quoi, mais il s'agit généralement d'un objet d'erreur héritant de la classe Error
intégrée. Plus d’informations sur l’extension des erreurs dans le chapitre suivant.
La relance est un modèle très important de gestion des erreurs : un bloc catch
attend généralement et sait comment gérer le type d'erreur particulier, il doit donc relancer les erreurs qu'il ne connaît pas.
Même si nous n'avons pas try...catch
, la plupart des environnements nous permettent de configurer un gestionnaire d'erreurs « global » pour détecter les erreurs qui « tombent ». Dans le navigateur, c'est window.onerror
.
importance : 5
Comparez les deux fragments de code.
Le premier utilise finally
pour exécuter le code après try...catch
:
essayer { travail travail } attraper (erreur) { gérer les erreurs } enfin { nettoyer l'espace de travail }
Le deuxième fragment place le nettoyage juste après try...catch
:
essayer { travail travail } attraper (erreur) { gérer les erreurs } nettoyer l'espace de travail
Nous avons absolument besoin d'un nettoyage après les travaux, peu importe qu'il y ait eu une erreur ou non.
Y a-t-il un avantage à utiliser finally
ou les deux fragments de code sont égaux ? S'il existe un tel avantage, donnez un exemple lorsque cela est important.
La différence devient évidente lorsque l’on regarde le code à l’intérieur d’une fonction.
Le comportement est différent s'il y a un « saut » de try...catch
.
Par exemple, lorsqu'il y a un return
à l'intérieur try...catch
. La clause finally
fonctionne en cas de sortie de try...catch
, même via l'instruction return
: juste après que try...catch
soit terminé, mais avant que le code appelant n'obtienne le contrôle.
fonction f() { essayer { alert('démarrer'); renvoyer « résultat » ; } attraper (erreur) { ///... } enfin { alert('nettoyage !'); } } f(); // nettoyage !
…Ou quand il y a un throw
, comme ici :
fonction f() { essayer { alert('démarrer'); throw new Error("une erreur"); } attraper (erreur) { //... if("je ne peux pas gérer l'erreur") { jeter l'erreur; } } enfin { alerte('nettoyage!') } } f(); // nettoyage !
C'est finally
cela qui garantit le nettoyage ici. Si nous mettons simplement le code à la fin de f
, il ne fonctionnera pas dans ces situations.