Accueil / Articles / Dix habitudes récurrentes en JavaScript qui sapent discrètement votre base de code

Dix habitudes récurrentes en JavaScript qui sapent discrètement votre base de code

Explique dix pièges courants en JavaScript et TypeScript, allant de l’égalité lâche à la mutation d’état, et présente des patterns plus sûrs pour remplacer chacun d’eux.

1748 mots

Presque tous les projets JavaScript, quel que soit l’entreprise ou le framework utilisé, présentent généralement le même petit ensemble de problèmes récurrents. Il s’agit rarement d’erreurs exotiques ou de cas limites imprévisibles. Ce sont plutôt les mêmes habitudes qui réapparaissent sans cesse.

Aucun de ces problèmes ne mettra immédiatement votre application en panne. C’est précisément pour cela qu’ils sont si dangereux. Ils restent latents jusqu’à ce que le projet s’étende, qu’un nouveau membre de l’équipe commence à modifier le code ou que l’utilisation augmente soudainement, et ce n’est qu’alors qu’ils émergent sous forme d’erreurs qui peuvent occuper toute une après-midi. Ci-dessous figurent les dix schémas les plus fréquents, accompagnés de solutions plus adaptées.

1. Utilisation de == au lieu de ===

L’opérateur d’égalité lâche en JavaScript force la conversion des types avant de les comparer, et les résultats sont notoirement difficiles à prédire :

0 == "0"        // true
0 == ""         // true
"" == "0"       // false
null == undefined // true

Bien sûr, il existe une logique interne à ces règles de coercition une fois qu’on les a mémorisées. Mais personne ne devrait avoir à garder ce modèle mental en mémoire juste pour écrire une simple condition. Préférez toujours === : il vérifie à la fois la valeur et le type, vous fournissant exactement la comparaison souhaitée, sans aucune conversion cachée.

if (userInput === "0") { ... }  // clear, predictable

2. Modifier directement l’état

Cette erreur entraîne des bugs particulièrement difficiles à détecter, car les symptômes apparaissent souvent loin de la cause réelle :

function addItem(cart, item) {
  cart.items.push(item); // mutates the original array
  return cart;
}

Si cet objet cart est observé ailleurs, au sein de l’état React, d’un store Redux ou de tout système qui détecte les changements en comparant des références, ce type de mutation en place reste complètement invisible pour lui. La référence elle-même ne change jamais, donc aucune réaffichage n’a lieu, aucun abonné n’est notifié, et vous finissez par essayer de mettre à jour une interface utilisateur qui refuse mystérieusement de s’actualiser.

function addItem(cart, item) {
  return { ...cart, items: [...cart.items, item] };
}

Créer un objet ou un tableau entièrement nouveau au lieu de modifier l’original consomme effectivement un peu plus de mémoire. En contrepartie, on obtient des changements d’état prévisibles, ce qui est un échange qui vaut la peine d’être fait bien plus souvent qu’on ne le pense.

3. Ne pas gérer les rejets de Promise

Une fonction async qui lance une exception sans être encadrée par try/catch, ou une chaîne .then() manquant de .catch(), a tendance à échouer silencieusement. Dans le navigateur, cela peut ne provoquer aucun effet visible ; dans Node, cela peut générer un avertissement d’erreur non gérée facile à manquer parmi les autres journaux.

async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    return await res.json();
  } catch (err) {
    logger.error("Failed to fetch user", { id, err });
    throw err;
  }
}

Toute fonction async susceptible d’échouer nécessite une stratégie explicite pour gérer ce cas d’échec. Le laisser non traité n’est pas vraiment une stratégie, c’est simplement une erreur prête à se manifester plus tard.

4. Appels de fonction imbriqués en profondeur

Nul ne cherche délibérément à créer un « enfer des appels de fonction ». Cela se développe progressivement, étape asynchrone après étape, jusqu’à ce que le code soit imbriqué à six niveaux de profondeur et que l’indentation ressemble à un escalier :

getUser(id, (user) => {
  getOrders(user.id, (orders) => {
    getShipping(orders[0].id, (shipping) => {
      updateUI(shipping); // and it keeps going
    });
  });
});

async/await a été introduit spécifiquement pour éliminer ce type de récursivité :

async function loadShippingInfo(id) {
  const user = await getUser(id);
  const orders = await getOrders(user.id);
  const shipping = await getShipping(orders[0].id);
  updateUI(shipping);
}

Le comportement asynchrone sous-jacent reste identique, mais la logique s’affiche maintenant de haut en bas, à peu près comme on la décrirait à voix haute.

5. Les variables globales qui s’infiltrent

Si l’on omet une déclaration const, let ou var en dehors du mode strict, JavaScript liera silencieusement cette variable à l’objet global au lieu de générer une erreur :

function calculateTotal() {
  total = 0; // no declaration — this is now global
  for (const item of items) total += item.price;
  return total;
}

La variable total se trouve désormais en dehors du champ d’application de la fonction, ce qui lui permet de entrer en conflit avec toute autre variable du même nom ailleurs dans le code, soit en écrasant une autre valeur, soit en étant elle-même écrasée selon l’ordre d’exécution. L’ajout de "use strict" en haut d’un fichier transforme cela en une erreur immédiate et visible, plutôt qu’en une erreur silencieuse et différée. La syntaxe moderne de modules utilisant import/export active automatiquement le mode strict, ce qui rend ce type d’erreur beaucoup moins fréquent lorsqu’on travaille avec des modules ES.

6. Comparaison d’objets et de tableaux avec ===

Cette erreur apparaît souvent chez ceux qui ont pris la règle n°1 un peu trop au sérieux. === compare les objets et les tableaux par référence, et non en fonction de leur contenu :

{ a: 1 } === { a: 1 }       // false
[1, 2, 3] === [1, 2, 3]     // false

Deux objets qui semblent identiques restent des entités distinctes en mémoire, c’est pourquoi l’égalité stricte les considère comme inégaux. Pour comparer leur contenu réel, il faut recourir à une approche de comparaison approfondie : un outil d’aide comme isEqual de lodash, JSON.stringify pour les cas simples, ou une routine de comparaison personnalisée. L’utilisation de === ici n’est pas une erreur de syntaxe, elle répond simplement à une question différente de celle que vous vouliez vraiment poser.

7. Oublier de nettoyer les écouteurs d’événements et les temporisateurs

Chaque appel à addEventListener, setInterval ou toute autre fonction de souscription représente une promesse selon laquelle quelque chose viendra finir par les nettoyer. En négligeant cette promesse, on crée une fuite mémoire, facile à manquer pendant le développement mais coûteuse une fois le système en production :

useEffect(() => {
  window.addEventListener("resize", handleResize);
  // no cleanup — this listener never goes away
}, []);
useEffect(() => {
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

8. Utilisation excessive de any en TypeScript

any n’est pas vraiment un type, mais plutôt une voie de sortie ; en l’utilisant trop souvent, on transforme discrètement une base de code fortement typée en une base non typée, sans que personne n’ait réellement choisi ce résultat :

function processPayment(data: any) {
  return data.amount * data.rate; // no safety net at all
}

Chaque fois que vous modifiez data ici, vous faites des suppositions. Le compilateur ne peut pas signaler d’erreur de frappe, d’attribut manquant ou de type incohérent, car vous lui avez explicitement demandé d’arrêter les vérifications. Même un type faiblement défini vaut mieux que pas de type du tout :

type PaymentData = { amount: number; rate: number };

function processPayment(data: PaymentData) {
  return data.amount * data.rate;
}

Si vous ne connaissez vraiment pas encore la structure d’un élément, unknown est l’équivalent honnête de any. Il vous oblige à préciser le type avant de pouvoir l’utiliser, au lieu de vous permettre d’agir sur des hypothèses non vérifiées.

9. Ignorer la différence entre null et undefined

JavaScript propose deux manières distinctes d’exprimer « rien ici », et les bases de code qui les utilisent de manière incohérente se retrouvent remplies d’opérations de vérification comme celle-ci :

if (value === null || value === undefined) { ... }

Ce schéma est généralement le signe que personne n’a jamais convenu d’une convention. Une approche plus ordonnée consiste à attribuer une signification distincte à chaque valeur : undefined signifie « cela n’a jamais été défini », tandis que null signifie « cela a été délibérément mis à rien ». À partir de là, l’opérateur de fusion des valeurs nulles vous permet de vérifier les deux en même temps, sans avoir à réécrire la comparaison à deux reprises :

const displayName = user.nickname ?? "Anonymous";

?? ne s’active que lorsque la valeur de gauche est null ou undefined. Cela diffère de ||, qui recourt également à 0, "" ou false, des valeurs souvent légitimes qui ne devraient pas être considérées comme manquantes.

10. Écrire du code pour l’ordinateur plutôt que pour la personne suivante

Le dernier élément de cette liste n’est pas une erreur de syntaxe, pourtant il cause plus de dommages cumulatifs que les neuf autres réunis. Une ligne de code très dense peut sembler gratifiante à écrire, mais elle est coûteuse pour quiconque doit la lire :

const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);

Cela fonctionne. Mais quiconque le lit par la suite, y compris vous dans quelques mois, doit faire de l’ingénierie inverse pour comprendre ce que a, x et le reste de la chaîne représentent réellement avant d’apporter des modifications sûres.

const activeUserBalances = users
  .filter((user) => user.isActive)
  .map((user) => user.balance);

const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);

Cette version nécessite quelques lignes supplémentaires, mais elle est immédiatement compréhensible sans aucun décodage requis. JavaScript a tendance à récompenser l’ingéniosité sur le moment avant de facturer les conséquences plus tard, et ce « plus tard », c’est presque toujours quelqu’un d’autre que celui qui a écrit le code à l’origine.

Le schéma commun à ces dix points

En mettant de côté les détails, aucun de ces dix éléments ne concerne vraiment des curiosités obscures du JavaScript. Ils portent tous sur la prévisibilité : des comparaisons qui se comportent comme indiqué, un état qui ne change pas en cachette, des erreurs qui sont détectées au lieu de disparaître silencieusement, et un code dont la structure correspond à ce qu’il fait réellement. En corrigeant ces dix habitudes, il ne reste plus que le débogage ordinaire, celui qui fait partie de toute base de code, plutôt que ce type de problèmes causés soi-même qui consomment discrètement votre après-midi.

Lectures complémentaires

  • Neuf habitudes au niveau du code qui rendent le travail des ingénieurs experts plus fiable — Décrit neuf pratiques de codage concrètes, allant des clauses de protection à un modélisation rigoureuse des données, qui rendent le code plus résilient, lisible et plus facile à déboguer en situation de pression.
  • Dix habitudes d’architecture qui permettent aux codebases frontend de rester maintenables pendant des années — Explique des habitudes structurelles telles que l’optimisation pour la supprimabilité, un flux de données explicite et l’isolation de la logique métier, qui aident les codebases à rester maintenables malgré des années de modifications.
  • Archéologie logicielle : une méthode pratique pour lire du code hérité — Découvrez une approche étape par étape pour examiner en toute sécurité des bases de code hérité non documentées, depuis l’analyse de l’historique des modifications jusqu’au refactoring sans perturber le fonctionnement en production.
  • Dix règles JavaScript du quotidien qui détruisent votre modèle mental — Découvrez dix comportements subtils de JavaScript – allant de la mutabilité de const à les closures et à la gestion des erreurs asynchrones – qui provoquent discrètement des bugs dans le code des développeurs expérimentés.
  • Sept idiomes communs de JavaScript qui introduisent subtilement des problèmes futurs — Explique comment les schémas courants de JavaScript tels que les vérifications de vérité, la chaînage optionnel et la syntaxe de déploiement cachent des hypothèses qui se brisent silencieusement à mesure que le code évolue.