12k
All articles

3 JavaScript-Fallstricke erklärt

Drei JavaScript-Fallen erklärt: Fließkomma, NaN-Prüfungen und await in Schleifen, plus die passenden Korrekturen.

OpenReplay Team
OpenReplay Team
3 JavaScript-Fallstricke erklärt

0.1 + 0.2 ist nicht gleich 0.3, NaN ist nicht gleich sich selbst, und await innerhalb einer Schleife kann eine schnelle Seite verlangsamen – drei JavaScript-Verhaltensweisen, die wie Bugs aussehen, aber tatsächlich genau dem entsprechen, was die Sprachspezifikation vorschreibt. Dieser Artikel erklärt den Mechanismus hinter jedem dieser Fälle – nicht nur die merkwürdige Konsolenausgabe – und zeigt die jeweils korrekte Lösung. Jeder dieser Fälle tritt als echtes, benutzerseitig sichtbares Problem auf: ein um einen Cent falscher Gesamtbetrag, eine Validierung, die fehlerhafte Eingaben durchlässt, oder eine Seite, die ohne ersichtlichen Grund langsam lädt. Das Warum zu verstehen ermöglicht es, alle drei Fälle im Produktivbetrieb zu vermeiden und sie in einem Interview präzise zu beantworten.

Wichtigste Erkenntnisse

  • 0.1 + 0.2 gibt 0.30000000000000004 zurück, weil IEEE 754 Doubles Zahlen im Binärformat speichern und weder 0.1 noch 0.2 eine exakte endliche Binärdarstellung besitzen – beide werden daher vor der Addition gerundet.
  • Für Geldbeträge sollte in ganzzahligen Cent-Werten statt in Gleitkomma-Dollarbeträgen gerechnet werden; für allgemeine Gleitkommavergleiche empfiehlt sich Math.abs(a - b) < tolerance mit einer auf die Größenordnung der Zahlen abgestimmten Toleranz – kein pauschales Number.EPSILON.
  • NaN === NaN ist false, weil die IEEE 754-Spezifikation NaN als ungleich zu jedem Wert – einschließlich sich selbst – definiert. Verwende Number.isNaN(), das keine Typumwandlung vornimmt, anstelle des globalen isNaN().
  • await innerhalb einer for-Schleife pausiert die gesamte Schleife bei jeder Iteration; starte die Promises zuerst und verwende await Promise.all(...), um unabhängige Anfragen gleichzeitig auszuführen.

Fallstrick #1: Warum 0.1 + 0.2 nicht 0.3 ergibt (Gleitkomma-Arithmetik)

0.1 + 0.2 gibt 0.30000000000000004 zurück, weil JavaScript-Zahlen IEEE 754 Double-Precision-Gleitkommazahlen sind, die zur Basis 2 gespeichert werden, und weder 0.1 noch 0.2 eine exakte endliche Binärdarstellung besitzt. Jedes Literal wird beim Schreiben auf den nächstgelegenen darstellbaren 64-Bit-Wert gerundet, und die Summe dieser beiden bereits gerundeten Werte ergibt eine Zahl, die knapp über 0.3 liegt.

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

Die letzte Zeile ist aufschlussreich: Der für 0.1 gespeicherte Wert war nie exakt 0.1. Dies ist keine JavaScript-Eigenheit – es ist eine Eigenschaft der Double-Precision-Gleitkommadarstellung, die Python, Java, C und jede andere Sprache teilt, die dasselbe Format verwendet. Die Zahl 0.1 ist im Binärsystem ein periodischer Bruch, genauso wie 1/3 im Dezimalsystem 0,333… ergibt und daher abgeschnitten werden muss.

Die Lösung hängt davon ab, was berechnet werden soll:

// Geldbeträge: in ganzzahligen Cent-Werten rechnen, nur für die Anzeige formatieren
const total = 1010 + 2030;   // 3040 Cent
(total / 100).toFixed(2);    // "30.40"

// Allgemeiner Vergleich: Toleranz an die Größenordnung anpassen
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

Für Währungsberechnungen sollte stets in ganzzahligen Cent-Werten gespeichert und gerechnet werden – niemals in Gleitkomma-Dollarbeträgen –, und erst an der Anzeigeschicht durch Division und toFixed(2) formatiert werden. Für Näherungsvergleiche wird gegen eine Toleranz verglichen. Number.EPSILON funktioniert nur für Zahlen in der Größenordnung von 1 – MDN warnt ausdrücklich davor, es als universellen Schwellenwert zu verwenden. Die Toleranz sollte daher an die Größenordnung der verglichenen Werte angepasst werden. Wer ein Array summiert: Math.sumPrecise() wurde im April 2026 als Baseline-Neuerung verfügbar, läuft daher möglicherweise nicht auf älteren Geräten. Zu beachten ist außerdem, dass diese Funktion das Präzisionsproblem bei 0.1 + 0.2 für einzelne Literale nicht behebt – sie verhindert lediglich die Fehlerakkumulation bei langen Summierungen.

Ein um einen Cent falscher Gesamtbetrag tritt nur bei der exakten Abfolge der summierten Werte auf. Deshalb ist eine Session-Replay-Aufzeichnung, die die tatsächliche Eingabereihenfolge rekonstruiert, hier nützlicher als ein Screenshot des falschen Betrags.

Fallstrick #2: NaN ist der einzige Wert, der nicht gleich sich selbst ist

NaN === NaN ist false, weil der IEEE 754-Standard NaN („Not-a-Number”) als ungleich zu jedem Wert – einschließlich sich selbst – definiert, und JavaScript diese Regel wörtlich umsetzt. Damit ist NaN der einzige Wert in der Sprache, der nicht gleich sich selbst ist – eine Eigenschaft, die sich sogar als NaN-Detektor nutzen lässt.

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

typeof NaN ist 'number': NaN ist ein numerischer Wert, der ein undefiniertes oder nicht darstellbares numerisches Ergebnis repräsentiert – kein eigenständiger Typ. Die eigentliche Falle ist das globale isNaN(), das sein Argument vor dem Test in eine Zahl umwandelt. Dadurch werden Werte, die gar kein NaN sind, so behandelt, als wären sie als Zahlen geprüft worden:

isNaN('');   // false  — '' wird zu 0 konvertiert
isNaN([]);   // false  — [] wird zu 0 konvertiert
isNaN('45'); // false  — '45' wird zu 45 konvertiert
isNaN({});   // true   — {} wird zu NaN konvertiert

Die Lösung ist Number.isNaN(), eingeführt in ES2015, das keine Typumwandlung vornimmt und nur für den tatsächlichen NaN-Wert true zurückgibt:

EingabeisNaN() (mit Konvertierung)Number.isNaN() (ohne Konvertierung)
NaNtruetrue
'NaN'truefalse
''falsefalse
[]falsefalse
'45'falsefalse
undefinedtruefalse

Number.isNaN() sollte verwendet werden, um zu prüfen „ist dies speziell der NaN-Wert”, und Number.isFinite(), wenn geprüft werden soll „ist dies eine reelle, endliche Zahl” – es lehnt NaN, Infinity und Nicht-Zahlen ohne Typumwandlung ab. Ein verwandter Mythos, der ausgeräumt werden sollte: parseInt("032") gibt 32 zurück, nicht 26. Die automatische Erkennung von Strings mit führender Null als Oktalzahlen wurde in ECMAScript 5 entfernt, daher ist die weit verbreitete Behauptung, das Ergebnis sei 26, ein Relikt aus der Zeit vor 2011. Ein Radix sollte dennoch angegeben werden – aber aus dem richtigen Grund.

Wenn eine Validierung fehlschlägt, weil isNaN('') einen leeren String zu 0 konvertiert hat, sieht der fehlerhafte Wert in einem Bug-Report oft „leer” aus. Das Nachspielen der tatsächlichen Tastatureingaben und des Feldzustands zeigt genau, was durchgerutscht ist.

Fallstrick #3: await in einer Schleife serialisiert Anfragen und verlangsamt die Benutzeroberfläche

await innerhalb einer for-Schleife pausiert die gesamte Schleife bei jeder Iteration, sodass Anfragen ohne gegenseitige Abhängigkeiten streng nacheinander statt parallel ausgeführt werden. Jede Iteration wartet darauf, dass ihr Promise abgeschlossen wird, bevor die nächste Anfrage überhaupt gestartet wird – aus N unabhängigen Roundtrips werden so N sequenzielle.

// Seriell: jedes await blockiert die nächste Iteration
async function getUsers(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // wartet ~1,5s, jedes Mal
  }
  return users;
}

Die Lösung besteht darin, alle Promises zuerst zu starten und sie dann gemeinsam mit Promise.all() abzuwarten. Dadurch werden die Anfragen gleichzeitig gestartet und aufgelöst, sobald alle abgeschlossen sind:

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

Bei einer simulierten festen Latenz von 1,5 Sekunden pro Anfrage (zur Veranschaulichung der Serialisierung, nicht als reale Netzwerkzeit) skaliert der Unterschied mit der Batchgröße:

Ansatz3 Anfragen10 Anfragen
await in einer Schleife (seriell)~4,5s~15s
Promise.all (gleichzeitig)~1,5s~1,5s

Ein Vorbehalt: Promise.all lehnt ab, sobald ein einzelnes Promise abgelehnt wird, und verwirft die übrigen Ergebnisse. Wenn ein einzelner Fehler den gesamten Batch nicht abbrechen soll, empfiehlt sich stattdessen Promise.allSettled(). Diese Methode wartet auf jedes Promise unabhängig vom Ergebnis und gibt ein Array zurück, das jeden Eintrag als fulfilled oder rejected ausweist – ideal, wenn mehrere unabhängige Ressourcen abgerufen werden und ein Teilerfolg akzeptabel ist. Promise.allSettled wurde in ES2020 eingeführt und ist seit Anfang 2023 browserübergreifend als Baseline verfügbar, sodass in modernen Umgebungen kein Polyfill benötigt wird. Serielles Ausführen ist nur dann sinnvoll, wenn jede Anfrage tatsächlich vom Ergebnis der vorherigen abhängt.

Eine Seite, die für manche Nutzer „einfach langsam” ist, lässt sich im Code-Review schwer erkennen. Eine Session-Replay-Aufzeichnung zeigt hingegen, wie Anfragen nacheinander statt gleichzeitig ausgelöst werden – und weist damit direkt auf eine await-Schleife hin.

Wie es weitergeht

Diese drei Verhaltensweisen sind spezifikationskonform – genau deshalb überstehen sie Code-Reviews und erreichen die Nutzer: Binäre Gleitkommazahlen runden, weil IEEE 754 es so vorschreibt; NaN lehnt sich selbst ab, weil der Standard es so definiert; und await serialisiert, weil das die Bedeutung von Ausführungspausen ist. Die Gegenmaßnahmen sind klein und mechanisch: ganzzahlige Cent-Werte und größenordnungsangepasste Toleranzen für Gleitkommazahlen, Number.isNaN() und Number.isFinite() für numerische Prüfungen, Promise.all oder Promise.allSettled für unabhängige asynchrone Aufgaben. Wer die eigene Währungslogik, Validierungsabsicherungen und Datenabruf-Schleifen gegen diese drei Muster prüft, verhindert, dass die Klasse von „unmöglichen” Bugs, die sie verursachen, weiterhin ausgeliefert wird.

Häufig gestellte Fragen

Was ist der Unterschied zwischen dem globalen isNaN() und Number.isNaN()?

Das globale isNaN() konvertiert sein Argument vor dem Test in eine Zahl, sodass isNaN('') und isNaN([]) beide false zurückgeben, weil '' und [] zu 0 konvertiert werden, während isNaN('NaN') true zurückgibt, weil 'NaN' zum NaN-Wert konvertiert wird. Number.isNaN(), eingeführt in ES2015, nimmt keine Typumwandlung vor und gibt true nur für den tatsächlichen NaN-Wert zurück. Verwende Number.isNaN(), wenn du speziell NaN erkennen möchtest.

Tritt das Gleitkomma-Problem mit 0.1 + 0.2 nur in JavaScript auf?

Nein. Es tritt in jeder Sprache auf, die IEEE 754 Double-Precision-Gleitkommazahlen verwendet, einschließlich Python, Java, C, C++ und Ruby. Weder 0.1 noch 0.2 besitzt eine exakte endliche Binärdarstellung, daher wird jede Zahl beim Speichern gerundet, und die Summe ergibt 0.30000000000000004. Es handelt sich um eine Eigenschaft des binären Gleitkommaformats selbst, nicht um einen JavaScript-Bug – daher gelten dieselben Lösungsansätze mit ganzzahligen Cent-Werten und toleranzbasiertem Vergleich sprachübergreifend.

Wann sollte Promise.allSettled statt Promise.all verwendet werden?

Verwende Promise.allSettled, wenn ein einzelner Fehler den gesamten Batch nicht abbrechen soll. Promise.all lehnt ab, sobald ein einzelnes Promise abgelehnt wird, und verwirft die Ergebnisse der übrigen – es eignet sich daher für Alles-oder-nichts-Operationen. Promise.allSettled wartet unabhängig vom Ergebnis auf jedes Promise und gibt ein Array zurück, das jeden Eintrag als fulfilled oder rejected ausweist. Das ist genau das Richtige, wenn mehrere unabhängige Ressourcen abgerufen werden und ein Teilerfolg akzeptabel ist. Es wurde in ES2020 eingeführt und benötigt in modernen Umgebungen kein Polyfill.

Ist es jemals korrekt, await innerhalb einer Schleife zu verwenden?

Ja, wenn jede Iteration tatsächlich vom Ergebnis der vorherigen abhängt – etwa beim seitenweisen Durchlaufen einer API, bei der die nächste Anfrage einen von der letzten Antwort zurückgegebenen Cursor benötigt, oder beim bewussten Drosseln, um einen Server nicht zu überlasten. In diesen Fällen ist die serielle Ausführung das gewünschte Verhalten. Der Fallstrick gilt nur für unabhängige Anfragen ohne gegenseitige Abhängigkeiten, bei denen das Warten in einer Schleife Arbeit unnötig serialisiert, die Promise.all gleichzeitig ausführen könnte.

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.