12k
All articles

3 Pièges JavaScript Expliqués

Trois pièges JavaScript expliqués : calcul en virgule flottante, tests de NaN et await dans les boucles, avec les bons correctifs.

OpenReplay Team
OpenReplay Team
3 Pièges JavaScript Expliqués

0.1 + 0.2 n’est pas égal à 0.3, NaN n’est pas égal à lui-même, et await dans une boucle peut transformer une page rapide en page lente — trois comportements JavaScript qui ressemblent à des bugs, mais qui correspondent en réalité à ce que la spécification du langage exige. Cet article explique le mécanisme derrière chacun d’eux, pas seulement la sortie console surprenante, et propose la correction appropriée pour chaque cas. Chacun se manifeste par un symptôme concret côté utilisateur : un total décalé d’un centime, une validation qui laisse passer des données incorrectes, un écran qui se charge lentement sans raison apparente. Comprendre le pourquoi est ce qui vous permet d’éviter ces trois pièges en production et d’y répondre avec précision lors d’un entretien technique.

Points Clés

  • 0.1 + 0.2 retourne 0.30000000000000004 parce que les doubles IEEE 754 stockent les nombres en binaire, et ni 0.1 ni 0.2 n’ont de représentation binaire finie exacte — chacun est donc arrondi avant d’être additionné.
  • Pour les montants monétaires, calculez en centimes entiers plutôt qu’en dollars à virgule flottante ; pour les comparaisons générales de flottants, testez Math.abs(a - b) < tolérance avec une tolérance adaptée à l’ordre de grandeur de vos nombres, et non un Number.EPSILON appliqué de façon systématique.
  • NaN === NaN vaut false parce que la spécification IEEE 754 définit NaN comme inégal à toute valeur, y compris lui-même — utilisez Number.isNaN(), qui n’effectue aucune coercition, plutôt que le isNaN() global.
  • await dans une boucle for met en pause toute la boucle à chaque itération ; démarrez d’abord les promesses et utilisez await Promise.all(...) pour exécuter des requêtes indépendantes de façon concurrente.

Piège n°1 : Pourquoi 0.1 + 0.2 n’est pas 0.3 (arithmétique en virgule flottante)

0.1 + 0.2 retourne 0.30000000000000004 parce que les nombres JavaScript sont des flottants double précision IEEE 754 stockés en base 2, et ni 0.1 ni 0.2 n’ont de représentation binaire finie exacte. Chaque littéral est arrondi à la valeur 64 bits représentable la plus proche dès son écriture, et la somme de ces deux valeurs déjà arrondies s’arrondit à un nombre légèrement supérieur à 0.3.

0.1 + 0.2;             // 0.30000000000000004
0.1 + 0.2 === 0.3;     // false
(0.1).toPrecision(20); // "0.10000000000000000555"

Cette dernière ligne est révélatrice : la valeur stockée pour 0.1 n’a jamais été exactement 0.1. Il ne s’agit pas d’une particularité de JavaScript — c’est une propriété de la virgule flottante double précision, partagée par Python, Java, C, et tout langage utilisant le même format. Le nombre 0.1 en binaire est une fraction périodique, de la même façon que 1/3 vaut 0.333… en décimal, et doit donc être tronqué.

La correction dépend de ce que vous calculez :

// Montants monétaires : travailler en centimes entiers, formater uniquement pour l'affichage
const total = 1010 + 2030;   // 3040 centimes
(total / 100).toFixed(2);    // "30.40"

// Comparaison générale : tolérance adaptée à l'ordre de grandeur
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

Pour les devises, stockez et calculez en centimes entiers, sans jamais utiliser de dollars en virgule flottante, puis divisez et appliquez toFixed(2) uniquement au moment de l’affichage. Pour l’égalité approximative, comparez par rapport à une tolérance. Number.EPSILON ne fonctionne que pour des nombres d’un ordre de grandeur proche de 1 — MDN avertit explicitement qu’il ne constitue pas un seuil universel fiable, il faut donc adapter la tolérance à la magnitude des valeurs comparées. Si vous faites la somme d’un tableau, Math.sumPrecise() est devenu disponible en tant que Baseline en avril 2026, et pourrait ne pas fonctionner sur des appareils plus anciens. Notez également qu’il ne peut pas résoudre le problème de précision de 0.1 + 0.2 pour des littéraux individuels — il prévient uniquement l’accumulation d’erreurs sur une longue somme.

Un total décalé d’un centime ne se reproduit qu’avec la séquence exacte de valeurs additionnées, ce qui explique pourquoi un outil de rejeu de session qui reconstruit l’ordre réel des saisies est plus utile ici qu’une capture d’écran du mauvais résultat.

Piège n°2 : NaN est la seule valeur qui n’est pas égale à elle-même

NaN === NaN vaut false parce que la norme IEEE 754 définit NaN (« Not-a-Number ») comme inégal à toute valeur, y compris lui-même, et JavaScript applique cette règle à la lettre. Cela fait de NaN la seule valeur du langage qui n’est pas égale à elle-même — une propriété que vous pouvez même exploiter comme détecteur de NaN.

NaN === NaN;   // false
NaN == NaN;    // false
typeof NaN;    // "number"

typeof NaN vaut 'number' : NaN est une valeur numérique représentant un résultat numérique indéfini ou non représentable, et non un type distinct. Le vrai piège est le isNaN() global, qui convertit son argument en nombre avant de tester, si bien que des valeurs qui ne sont pas NaN du tout sont signalées comme si elles avaient été évaluées numériquement :

isNaN('');   // false  — '' est converti en 0
isNaN([]);   // false  — [] est converti en 0
isNaN('45'); // false  — '45' est converti en 45
isNaN({});   // true   — {} est converti en NaN

La correction est Number.isNaN(), introduit en ES2015, qui n’effectue aucune coercition et retourne true uniquement pour la valeur NaN elle-même :

EntréeisNaN() (avec coercition)Number.isNaN() (sans coercition)
NaNtruetrue
'NaN'truefalse
''falsefalse
[]falsefalse
'45'falsefalse
undefinedtruefalse

Utilisez Number.isNaN() pour tester « est-ce spécifiquement la valeur NaN », et Number.isFinite() lorsque vous voulez vérifier « est-ce un nombre réel et fini » — il rejette NaN, Infinity et les non-nombres sans coercition. Un mythe connexe mérite d’être définitivement écarté : parseInt("032") retourne 32, et non 26. La détection automatique des chaînes à zéro initial comme valeurs octales a été supprimée dans ECMAScript 5, donc l’affirmation largement reprise donnant 26 est un vestige d’avant 2011 — passez tout de même un radix, mais pour la bonne raison.

Lorsqu’une validation échoue parce que isNaN('') a converti une chaîne vide en 0, la valeur fautive apparaît souvent comme « vide » dans un rapport de bug ; rejouer les frappes réelles et l’état du champ montre exactement ce qui a été accepté à tort.

Piège n°3 : await dans une boucle sérialise les requêtes et ralentit votre interface

await dans une boucle for met en pause toute la boucle à chaque itération, de sorte que des requêtes sans dépendance mutuelle s’exécutent strictement les unes après les autres au lieu de s’exécuter en parallèle. Chaque itération attend que sa promesse soit résolue avant même que la requête suivante ne démarre, transformant N allers-retours indépendants en N allers-retours séquentiels.

// Séquentiel : chaque await bloque l'itération suivante
async function getUsers(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // attend ~1,5s à chaque fois
  }
  return users;
}

La correction consiste à démarrer toutes les promesses en premier, puis à les attendre ensemble avec Promise.all(), qui déclenche les requêtes de façon concurrente et se résout une fois qu’elles ont toutes abouti :

async function getUsers(ids) {
  return Promise.all(ids.map(fetchUser));
}

Avec une latence fixe simulée de 1,5 seconde par requête (illustrant la sérialisation, et non des temps réseau réels), la différence s’accentue avec la taille du lot :

Approche3 requêtes10 requêtes
await dans une boucle (séquentiel)~4,5s~15s
Promise.all (concurrent)~1,5s~1,5s

Une mise en garde : Promise.all rejette dès qu’une seule promesse est rejetée, abandonnant les autres. Lorsqu’un échec ne doit pas interrompre l’ensemble du lot, utilisez plutôt Promise.allSettled(), qui attend toutes les promesses et rapporte chacune comme fulfilled ou rejected. Promise.allSettled est disponible depuis ES2020 et fait partie du socle commun des navigateurs depuis début 2023, il ne nécessite donc aucun polyfill dans les environnements modernes. Ne sérialisez délibérément que lorsque chaque requête dépend réellement du résultat de la précédente.

Un écran « juste lent » pour certains utilisateurs est difficile à détecter lors d’une revue de code ; un outil de rejeu de session révèle des requêtes qui se déclenchent les unes après les autres au lieu de se déclencher simultanément, ce qui pointe directement vers une boucle avec await.

Pour aller plus loin

Ces trois comportements sont conformes à la spécification, ce qui explique précisément pourquoi ils survivent aux revues de code et atteignent les utilisateurs : les flottants binaires s’arrondissent parce que IEEE 754 l’impose, NaN se rejette lui-même parce que la norme le définit ainsi, et await sérialise parce que c’est ce que signifie la mise en pause de l’exécution. Les remèdes sont simples et méthodiques — centimes entiers et tolérances adaptées à l’ordre de grandeur pour les flottants, Number.isNaN() et Number.isFinite() pour les vérifications numériques, Promise.all ou Promise.allSettled pour les traitements asynchrones indépendants. Auditez votre propre code de calcul monétaire, vos gardes de validation et vos boucles de récupération de données selon ces trois schémas, et la catégorie de bugs « impossibles » qu’ils engendrent cessera d’être déployée en production.

Questions Fréquentes

Quelle est la différence entre le isNaN() global et Number.isNaN() ?

Le isNaN() global convertit son argument en nombre avant de tester, si bien que isNaN('') et isNaN([]) retournent tous deux false parce que '' et [] sont convertis en 0, tandis que isNaN('NaN') retourne true parce que 'NaN' est converti en la valeur NaN. Number.isNaN(), introduit en ES2015, n'effectue aucune coercition et retourne true uniquement pour la valeur NaN elle-même. Utilisez Number.isNaN() lorsque vous avez spécifiquement besoin de détecter NaN.

Le problème de virgule flottante avec 0.1 + 0.2 se produit-il uniquement en JavaScript ?

Non. Il se produit dans tout langage utilisant des flottants double précision IEEE 754, notamment Python, Java, C, C++ et Ruby. Ni 0.1 ni 0.2 n'ont de représentation binaire finie exacte, chacun est donc arrondi lors du stockage, et leur somme s'arrondit à 0.30000000000000004. C'est une propriété du format binaire à virgule flottante lui-même, et non un bug JavaScript — les mêmes corrections par centimes entiers et par tolérance s'appliquent donc dans tous les langages.

Quand dois-je utiliser Promise.allSettled plutôt que Promise.all ?

Utilisez Promise.allSettled lorsqu'un échec ne doit pas interrompre l'ensemble du lot. Promise.all rejette dès qu'une seule promesse est rejetée et abandonne les résultats des autres, ce qui convient aux opérations de type tout-ou-rien. Promise.allSettled attend toutes les promesses quelle que soit leur issue et retourne un tableau indiquant chacune comme fulfilled ou rejected, ce qui est approprié lorsque vous récupérez plusieurs ressources indépendantes et qu'un succès partiel est acceptable. Il est disponible depuis ES2020 et ne nécessite aucun polyfill dans les environnements modernes.

Est-il parfois correct d'utiliser await dans une boucle ?

Oui, lorsque chaque itération dépend réellement du résultat de la précédente — par exemple lors de la pagination d'une API où la requête suivante nécessite un curseur retourné par la dernière réponse, ou lors d'une limitation délibérée du débit pour ne pas surcharger un serveur. Dans ces cas, l'exécution séquentielle est le comportement souhaité. Le piège ne s'applique qu'aux requêtes indépendantes sans dépendance mutuelle, pour lesquelles attendre dans une boucle sérialise inutilement un travail que Promise.all pourrait exécuter de façon concurrente.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.