Gérer les Erreurs et les Exceptions en JavaScript

Une étape bien trop souvent oubliée lors de la réalisation de scripts JavaScript est la gestion des erreurs. On se contente de colmater les problèmes à l'aide de try/catch quand ceux-ci sont remontés par l'interpréteur et... et c'est tout.

'Erreur ou Exception ?'

Voici les 3 principaux patterns a utilisés pour gérer vos erreurs JavaScript dans les navigateurs ou dans Node.js et pour ceux qui ont le temps, un petit topo sur la différence entre Erreur et Exception.

Erreur ou Exception ?

En JavaScript les Erreurs sont un type d'objet à elles seules et se créent en utilisant la syntaxe suivante new Error(). Elles se manipulent comme un objet et peuvent être return ou mises dans des variables. L'exécuteur JavaScript lui-même retourne des erreurs.

Les Exceptions quant à elles sont des Erreurs qui sont lancées ou jetées avec le mot clé throw soit throw new Error() et ne peuvent plus être manipulées. Elles mettent ainsi fin aux contextes d'exécution les un après les autres en remontant jusqu'à afficher une erreur dans la console. Elles peuvent être interceptées avec try et l'erreur qu'elles remontent peut être manipulée via catch (exception).

Ne pas confondre erreurs de développement et erreurs opérationnelles

Tout d'abord, avant de pouvoir correctement gérer les erreurs, il va falloir faire la différence entre celles de type opérationnelles et celles de développement.

Erreurs de syntaxe et d'éxécution

Les erreurs de développement sont des erreurs de syntaxe ou des bogues dans le programme. Ce sont des lignes qui peuvent toujours être réparées en changeant du code. Elles n'ont jamais besoin d'être interceptées. Ce sont des erreurs comme par exemple :

  • Oublier une parenthèse (erreur de syntaxe).
  • Essayer de lire la valeur d'une variable inexistante (erreur d'exécution).
  • Création d'une fonction asynchrone sans proposer de callback (erreur de conception).
  • Passer un primitif number quand un objet est attendu (erreur d'exécution).
  • etc...

Ajouter du code pour corriger ces erreurs n'est jamais la solution, sinon c'est la porte ouverte aux Abstractions qui Fuient.

Erreurs opérationnelles

Les erreurs opérationnelles sont les erreurs lancées par des programmes correctement écrit, ce ne sont pas des bogues dans les programmes mais des problèmes avec le système lui-même (hors mémoire, trop de fichiers ouvert, ...), la configuration système (pas de route pour l'adresse demandée, ...), le réseau (socket refermée, ...), les services distants (erreur 500, connexion impossible, ...) ou les utilisateurs (des inputs non valides) et ce sont elles que vous devez veiller à intercepter pour les traiter ou à renvoyer pour que d'autres fonctions puissent les traiter.

Erreur : définir, retourner et utiliser des erreurs

Il y a donc 3 façons de « lancer » des erreurs opérationnelles en toutes sécurités, chacune dépendant de votre implémentation JavaScript.

Pour du code synchrone

Pour du code synchrone, si une erreur doit être levée, elle est retournée de la manière suivante :

// Définition d'une fonction de division synchrone.
var divideSync = function (x, y) {
    "use strict";

    // La division par 0 est décidée comme interdite.
    if (y === 0) {

        // On lève proprement une erreur en la retournant.
        return (new Error("Can't divide by zero")).code = "ENOZER";
    }

    // S'il n'y a pas d'erreur, on retourne le résultat.
    return x / y;
};

et on traite l'erreur de cette façon :

(function () {
    "use strict";

    // Diviser 4/0.
    var result = divideSync(4, 0);

    // Est-ce qu'une erreur connue a été levée ?
    if (result instanceof Error && result.code === "ENOZER") { // Juste `(result instanceof Error)` pour une erreur inconnue.

        // Traiter l'erreur.
        return console.log("4/0 = Error, ", result); // 4/0 = Error, Can't divide by zero
    }

    // S'il n'y a pas d'erreur, on retourne le résultat.
    console.log("4/0 = " + result);
}());
Pour du code basé sur une Callback

Pour du code basé sur une callback (utilisée pour les fonctions asynchrones entre autre), le premier paramètre de la Callback est err. Si une erreur doit être levée, err est un new Error(), sinon, err est null. Après, n'importe quels types de paramètre peuvent suivre :

// Définition d'une fonction de division avec callback.
var divide = function(x, y, next) {
    "use strict";

    // La division par 0 est décidée comme interdite.
    if (y === 0) {

        // On lève une erreur proprement en appelant la callback
        // avec en premier paramètre l'erreur souhaitée.
        return next((new Error("Can't divide by zero")).code = "ENOZER");
    }

    // S'il n'y a pas d'erreur, on retourne le résultat.
    next(null, x / y);
};

et on traite l'erreur de cette façon :

divide(4, 0, function (err, result) {

    // Est-ce qu'une erreur connue a été levée ?
    if (err && err.code === "ENOZER") { // Juste `(err)` pour une erreur inconnue.

        // Traiter l'erreur.
        return console.log("4/0 = Error, ", err); // 4/0 = Error, Can't divide by zero
    }

    // S'il n'y a pas d'erreur, on retourne le résultat.
    console.log("4/0 = " + result);
});
Pour un Événement

Pour un code événementiel, si une erreur doit être levée, un événement de type error est émis.

En premier lieux, ajoutons la possibilité d’émettre et d'écouter des événements à un objet.

// Définition d'un événement de division.
var events = require('events'), // Utiliser EventEmitter côté client `http://smalljs.org/object/events/event-emitter/`
    Divider = function () {
        "use strict";

        events.EventEmitter.call(this);
    };
require('util').inherits(Divider, events.EventEmitter);

puis ajoutons une fonctionnalité de division à notre objet :

// Ajout d'une fonction de division.
Divider.prototype.divide = function (x, y) {
    "use strict";

    // La division par 0 est décidée comme interdite.
    if (y === 0) {

        // On lève proprement une erreur en l’émettant.
        this.emit("error", (new Error("Can't divide by zero")).code = "ENOZER");
    } else {

        // S'il n'y a pas d'erreur, on retourne le résultat.
        this.emit("divided", x, y, x / y);
    }

    // Permettre le chaînage.
    return this;
};

et on traite l'erreur de cette façon :

// Créer un nouveau diviseur.
var divider = new Divider();

// Gérer les erreurs.
divider.on("error", function (err) {
    "use strict";

    // Est-ce qu'une erreur connue a été levée ?
    if (err.code === "ENOZER") { // Pas de condition pour une erreur inconnue.
        // Traiter l'erreur.
        return console.log("4/2=err", err);
    }
});

// Gérer le résultat.
divider.on('divided', function (x, y, result) {
    "use strict";

    // S'il n'y a pas d'erreur, on retourne le résultat.
    console.log("4/2=" + result);
});

// Division réussi puis division levant une erreur après chaînage.
divider.divide(4, 2).divide(4, 0);

Exception : intercepter et lancer des erreurs avec try, catch et throw.

L'exemple qui suit ne fait pas partie des 3 patterns de bonne pratique dont nous avons parlé un peu plus tôt mais il est important de le connaître car ce n'est qu'ainsi que vous pourrez créer ce que l'on appel des exceptions et que vous pourrez intercepter celles lancées par l'interpréteur JavaScript.

Lancer une Exception

Vous ne devriez que rarement (voir jamais) utiliser le mot clé throw. Ce mot-clé transforme les new Error() en exceptions et ont pour but d'afficher des erreurs dans les consoles d'erreur et de mettre fin à l'interprétation du code restant, c'est pourquoi elles ne devraient jamais être utilisée pour représenter des erreurs opérationnelles. De plus, il est impossible d'utiliser try / catch pour intercepter une erreur lancée autour d'une fonction asynchrone.

Voici donc le throw pour lancer une erreur, c-à-d créer une exception :

throw new Error("The exception message.");

Intercepter une Exception

Pour intercepter une exception il suffit d'utiliser les mots-clés try / catch :

try {
    var code = "EXAMPL";

    throw new Error("Simulation of an Error throwed.");
} catch (exception) {
    return exception.code; // "EXAMPL"
}

À vous de jouer !