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.
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
- Dangers communs de JavaScript et TypeScript qui détruisent le code en secret — Explique les pièges subtils de JavaScript et TypeScript, allant des comparaisons avec NaN aux problèmes de synchronisation asynchrone et à la coercition de types, qui provoquent des erreurs bien que le code paraisse correct.
- Fonctionnalités de Node.js 26 qui substituent en secret des années de solutions provisoires — Une présentation de l’API Temporal de Node.js 26, de l’exécution native de TypeScript, des outils d’aide à la mise en cache et d’autres ajouts qui éliminent des solutions provisoires utilisées depuis longtemps.