warum 0.1 + 0.2 nicht 0.3 ergibt
Die Konsole gibt 0.30000000000000004 aus, ein Preisvergleich schlägt fehl, eine Summe von Rechnungsposten weicht um einen Cent ab. Gleitkomma-Präzision ist keine Eigenheit von JavaScript, sondern eine Konsequenz von IEEE 754, betrifft aber gerade JavaScript-Entwickler besonders oft, weil die Sprache nur einen einzigen Zahlentyp kennt. Dieser Artikel erklärt die Ursache und zeigt robuste Lösungen.
Inhaltsverzeichnis
- 1. Warum 0.1 + 0.2 nicht 0.3 ergibt
- 2. IEEE 754: wie JavaScript Zahlen tatsächlich speichert
- 3. Vergleiche mit Toleranz: Number.EPSILON richtig einsetzen
- 4. Rundungsfehler bei Geldbeträgen: der teuerste Fallstrick
- 5. toFixed und seine eigenen Fallstricke
- 6. Number.isInteger und sichere Ganzzahlgrenzen
- 7. BigInt als Ausweg für ganzzahlige Präzision
- 8. Bibliotheken und Alternativen für exakte Dezimalarithmetik
- 9. Ansätze gegen Gleitkomma-Präzisionsfehler im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum 0.1 + 0.2 nicht 0.3 ergibt
Kaum ein Codebeispiel demonstriert Gleitkomma-Präzision so eindrücklich wie 0.1 + 0.2 in der JavaScript-Konsole, das Ergebnis lautet 0.30000000000000004 statt der erwarteten 0.3. Für Entwickler, die zum ersten Mal darauf stoßen, wirkt das wie ein Bug in der Sprache, tatsächlich ist es eine direkte Konsequenz davon, wie Computer Dezimalzahlen im Binärsystem darstellen. Die Gleitkomma-Präzision von JavaScript ist dabei keine Besonderheit der Sprache selbst, sondern folgt dem Standard IEEE 754, den auch Java, Python, C und praktisch jede andere moderne Sprache für ihre Standard-Zahlentypen verwendet.
Der entscheidende Unterschied zu vielen anderen Sprachen ist, dass JavaScript nur einen einzigen numerischen Grundtyp kennt, den 64-Bit-Gleitkommatyp double aus IEEE 754. Es gibt in JavaScript kein separates int, keine eingebaute Dezimalarithmetik ohne zusätzliche Bibliothek, und keine Möglichkeit, die Gleitkomma-Präzision für einfache Ganzzahlberechnungen zu umgehen, ohne bewusst auf andere Datentypen wie BigInt auszuweichen. Dadurch treffen JavaScript-Entwickler auf Gleitkomma-Präzisionsprobleme früher und häufiger als Entwickler in Sprachen mit getrennten Integer- und Dezimaltypen.
Die folgenden Abschnitte erklären zunächst die technische Ursache über IEEE 754, zeigen dann robuste Vergleichs- und Rundungsstrategien und gehen schließlich auf BigInt und externe Bibliotheken als vollständige Lösungswege für exakte Arithmetik ein.
2. IEEE 754: wie JavaScript Zahlen tatsächlich speichert
Der IEEE-754-Standard stellt eine Zahl über drei Komponenten dar: ein Vorzeichenbit, einen Exponenten und eine Mantisse, insgesamt 64 Bit für den Doppelpräzisionstyp, den JavaScript für jede Zahl verwendet. Diese binäre Darstellung kann jede ganze Zahl innerhalb ihres Wertebereichs exakt abbilden, viele Dezimalbrüche jedoch nicht, aus demselben Grund, aus dem sich ein Drittel im Dezimalsystem nicht exakt als endliche Dezimalzahl schreiben lässt. 0.1 und 0.2 gehören zu genau dieser Kategorie: Ihre binäre Darstellung ist eine unendlich periodische Bruchzahl, die auf 64 Bit abgeschnitten werden muss, wodurch bereits vor der eigentlichen Addition ein winziger Rundungsfehler entsteht.
Werden zwei bereits gerundete Werte addiert, addieren sich auch ihre winzigen Rundungsfehler, und das Ergebnis weicht um einen für Menschen sichtbaren, wenn auch mathematisch winzigen Betrag vom exakten Resultat ab. Diese Gleitkomma-Präzision betrifft nicht nur Additionen, sondern jede Operation mit nicht binär exakt darstellbaren Dezimalbrüchen, einschließlich Subtraktion, Multiplikation und Division. Wichtig zu verstehen ist, dass dies kein Fehler in der JavaScript-Engine ist, sondern eine unvermeidbare mathematische Eigenschaft der binären Gleitkommadarstellung selbst.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// Not unique to addition — subtraction and multiplication show it too
console.log(0.3 - 0.1); // 0.19999999999999998
console.log(0.1 * 3); // 0.30000000000000004
// The exact IEEE 754 representation, shown with enough decimal digits
console.log((0.1).toFixed(20)); // 0.10000000000000000555
console.log((0.2).toFixed(20)); // 0.20000000000000001110
// Both are already rounded before the addition even happens
3. Vergleiche mit Toleranz: Number.EPSILON richtig einsetzen
Weil Gleitkomma-Präzisionsfehler direkte Gleichheitsvergleiche wie 0.1 + 0.2 === 0.3 unzuverlässig machen, ist der direkte Vergleich zweier berechneter Fließkommazahlen mit === grundsätzlich riskant, sobald mindestens eine der Zahlen aus einer Berechnung mit nicht binär exakten Dezimalbrüchen stammt. Die etablierte Lösung ist ein Toleranzvergleich, bei dem geprüft wird, ob die absolute Differenz zweier Werte kleiner ist als ein sehr kleiner Schwellenwert, statt exakte Gleichheit zu verlangen.
Number.EPSILON liefert genau diesen Schwellenwert als kleinste darstellbare Differenz zwischen 1 und der nächstgrößeren darstellbaren Zahl, und eignet sich als Ausgangspunkt für einen Toleranzvergleich bei Zahlen in der Größenordnung von eins. Bei deutlich größeren oder kleineren Zahlen muss der Schwellenwert entsprechend skaliert werden, weil die absolute Größe des Rundungsfehlers mit der Größenordnung der beteiligten Zahlen wächst. Ein pauschaler, fest verdrahteter Toleranzwert ohne Bezug zur tatsächlichen Größenordnung der Zahlen führt deshalb selbst wieder zu neuen, subtilen Fehlern.
// WRONG: exact equality fails due to floating point precision
console.log(0.1 + 0.2 === 0.3); // false
// RIGHT: tolerance-based comparison using Number.EPSILON
function nearlyEqual(a, b, epsilon = Number.EPSILON * 8) {
return Math.abs(a - b) < epsilon;
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
// For larger magnitudes, scale the tolerance relative to the values
function nearlyEqualScaled(a, b, relativeEpsilon = 1e-10) {
const diff = Math.abs(a - b);
const scale = Math.max(Math.abs(a), Math.abs(b), 1);
return diff <= relativeEpsilon * scale;
}
console.log(nearlyEqualScaled(1000000.1 + 0.2, 1000000.3)); // true
4. Rundungsfehler bei Geldbeträgen: der teuerste Fallstrick
Der wirtschaftlich teuerste Fallstrick der Gleitkomma-Präzision entsteht, wenn Geldbeträge direkt als Fließkommazahlen in Euro oder Dollar gespeichert und addiert werden. Ein Warenkorb mit vielen Positionen zu je 0.10 oder 0.20 Einheiten kumuliert exakt dieselben winzigen Rundungsfehler wie im einführenden Beispiel, und am Ende einer langen Rechnungssumme kann eine Differenz von einem Cent auftreten, die in Buchhaltungssystemen als Diskrepanz auffällt und manuelle Nachforschung auslöst.
Die bewährte Lösung in der Finanzbranche ist, Geldbeträge intern nicht in der Hauptwährungseinheit, sondern in der kleinsten Untereinheit als Ganzzahl zu speichern, in Euro also als Cent-Betrag. Ganzzahlen im sicheren Integer-Bereich von JavaScript sind durch IEEE 754 exakt darstellbar, sodass Gleitkomma-Präzisionsfehler bei der Addition und Subtraktion von Cent-Beträgen vollständig entfallen. Erst bei der finalen Anzeige wird der Cent-Betrag durch 100 geteilt und für den Nutzer formatiert, sodass keine Zwischenberechnung jemals mit Bruchteilen der Hauptwährung arbeitet.
// WRONG: accumulating floating point precision errors across many items
const pricesInEuro = [19.99, 5.5, 3.33, 0.99];
const totalWrong = pricesInEuro.reduce((sum, price) => sum + price, 0);
console.log(totalWrong); // 29.81 in this case, but can drift with more items
// RIGHT: store money as integer cents, no fractional main unit ever appears
const pricesInCents = [1999, 550, 333, 99];
const totalCents = pricesInCents.reduce((sum, cents) => sum + cents, 0);
console.log(totalCents / 100); // 29.81 — reliably exact, formatted only at the end
function formatEuro(cents) {
return (cents / 100).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });
}
console.log(formatEuro(totalCents)); // "29,81 €"
5. toFixed und seine eigenen Fallstricke
toFixed() wird oft als schnelle Lösung gegen Gleitkomma-Präzisionsprobleme herangezogen, hat aber selbst eigene Eigenheiten, die für Überraschungen sorgen. Die Methode gibt immer einen String zurück, keine Zahl, was bei anschließender arithmetischer Weiterverarbeitung zu impliziter Typumwandlung und subtilen Fehlern führt, wenn der Rückgabewert nicht explizit zurück in eine Zahl konvertiert wird. Zusätzlich rundet toFixed() nicht immer nach der klassischen kaufmännischen Regel, weil auch hier die zugrunde liegende Gleitkomma-Präzision der Eingabezahl das Rundungsergebnis beeinflusst.
Ein konkretes Beispiel: (1.005).toFixed(2) liefert in den meisten Engines "1.00" statt des erwarteten "1.01", weil 1.005 in der binären Gleitkommadarstellung tatsächlich als eine minimal kleinere Zahl gespeichert wird, die beim Runden nach unten kippt. Wer verlässliche kaufmännische Rundung benötigt, sollte entweder auf ganze Untereinheiten wie im vorherigen Abschnitt umsteigen oder eine dedizierte Rundungsfunktion verwenden, die den Eingabewert vor der Rundung durch Multiplikation und explizite Nachkorrektur der Gleitkomma-Präzision absichert.
// toFixed() returns a string, not a number — easy to forget
const price = 19.5;
console.log(typeof price.toFixed(2)); // "string", not "number"
console.log(price.toFixed(2) + 1); // "19.501" — string concatenation, not addition!
// Floating point precision can make toFixed() round the "wrong" way
console.log((1.005).toFixed(2)); // "1.00" — not "1.01" as naively expected
// Reason: 1.005 is actually stored as ~1.00499999999999989...
// A more robust rounding helper for two decimal places
function roundToCents(value) {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
console.log(roundToCents(1.005)); // 1.01 — corrects for the representation error
6. Number.isInteger und sichere Ganzzahlgrenzen
Auch reine Ganzzahlberechnungen sind nicht unbegrenzt sicher, weil der 64-Bit-Gleitkommatyp nur bis zu einer bestimmten Grenze jede Ganzzahl exakt darstellen kann. Number.MAX_SAFE_INTEGER liegt bei 2 hoch 53 minus 1, weil die Mantisse des IEEE-754-Formats 53 Bit für die Darstellung von Ganzzahlen zur Verfügung stellt. Oberhalb dieser Grenze können mehrere unterschiedliche mathematische Ganzzahlen auf denselben Gleitkommawert abgebildet werden, wodurch Vergleiche und Additionen still falsche Ergebnisse liefern, ohne dass ein Fehler geworfen wird.
Number.isSafeInteger() prüft explizit, ob ein Wert innerhalb dieser sicheren Grenze liegt und tatsächlich eine Ganzzahl ist, was gerade bei der Verarbeitung von IDs aus externen Systemen wichtig ist, etwa Datenbank-IDs oder Snowflake-IDs, die in JSON häufig als große Zahlen statt als Strings übertragen werden. Wird eine solche ID oberhalb der sicheren Ganzzahlgrenze als JavaScript-Zahl geparst, kann sich der Wert durch die Gleitkomma-Präzision unbemerkt verändern, mit der Konsequenz, dass Datensätze anhand der falschen ID abgefragt werden.
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.isSafeInteger(2 ** 53)); // false — right above the safe limit
// Precision loss above the safe integer range — a real-world API pitfall
const idFromApi = 9007199254740993; // JSON often sends large IDs as plain numbers
console.log(idFromApi); // 9007199254740992 — already wrong, silently
// Guard before trusting a numeric ID from an external source
function assertSafeId(value) {
if (!Number.isSafeInteger(value)) {
throw new Error(`ID ${value} exceeds safe integer precision — use a string or BigInt`);
}
return value;
}
7. BigInt als Ausweg für ganzzahlige Präzision
BigInt löst genau das im vorherigen Abschnitt beschriebene Problem für ganzzahlige Werte, indem es Ganzzahlen beliebiger Größe exakt darstellt, unabhängig von der 53-Bit-Grenze des regulären Number-Typs. Ein BigInt-Literal wird durch das Suffix n markiert, etwa 9007199254740993n, und alle arithmetischen Operationen zwischen zwei BigInt-Werten bleiben exakt, ohne jede Gleitkomma-Präzisionsproblematik, weil BigInt intern nicht auf IEEE 754 basiert, sondern eine beliebig lange Ganzzahlarithmetik implementiert.
Die Einschränkung von BigInt ist, dass es ausschließlich Ganzzahlen darstellt, keine Nachkommastellen, und dass es sich nicht ohne explizite Konvertierung mit regulären Number-Werten mischen lässt, ein direkter Vergleich mit + zwischen BigInt und Number wirft einen TypeError. Für Anwendungsfälle mit sehr großen Ganzzahlen, etwa kryptografische Berechnungen, Datenbank-IDs oder Zähler, die die sichere Ganzzahlgrenze überschreiten können, ist BigInt dennoch die robusteste native Lösung, ganz ohne externe Bibliothek und ohne jede Gleitkomma-Präzisionseinbuße.
const bigId = 9007199254740993n; // BigInt literal, exact even above MAX_SAFE_INTEGER
console.log(bigId + 1n); // 9007199254740994n — exact, no precision loss
// Mixing BigInt and Number directly throws
try {
console.log(bigId + 1); // TypeError: Cannot mix BigInt and other types
} catch (error) {
console.log(error.message);
}
// Explicit conversion is required in both directions
console.log(bigId + BigInt(1)); // 9007199254740994n
console.log(Number(bigId) + 1); // precision loss reappears once converted back
8. Bibliotheken und Alternativen für exakte Dezimalarithmetik
Für Anwendungsfälle, die sowohl exakte Ganzzahl- als auch exakte Dezimalarithmetik benötigen, etwa Finanzsysteme mit Zinsberechnungen oder wissenschaftliche Anwendungen, reicht weder der native Cent-Ansatz noch BigInt allein aus, weil beide entweder auf Ganzzahlen beschränkt sind oder eine manuelle Skalierung erfordern. Für diese Fälle existieren dedizierte Dezimalarithmetik-Bibliotheken wie decimal.js oder big.js, die Zahlen intern als String-basierte, beliebig genaue Dezimalwerte repräsentieren und dadurch jede Gleitkomma-Präzisionsproblematik der nativen Number-Arithmetik vollständig umgehen.
Der Nachteil dieser Bibliotheken ist der zusätzliche Rechenaufwand gegenüber nativen Gleitkommaoperationen, weil jede Operation über String-Parsing und eine eigene Arithmetik-Implementierung läuft, statt direkt auf Hardware-Gleitkommabefehle zuzugreifen. Für die überwiegende Mehrheit der Webanwendungen ist dieser Mehraufwand vernachlässigbar gegenüber dem Risiko fehlerhafter Finanzberechnungen, und der Einsatz einer solchen Bibliothek lohnt sich immer dann, wenn Berechnungen mit mehr als zwei Dezimalstellen, komplexe Rundungsregeln oder eine sehr hohe Anzahl von Additionsschritten erforderlich sind.
// Conceptual example using a decimal arithmetic library (e.g. decimal.js)
// import Decimal from 'decimal.js';
// const a = new Decimal('0.1');
// const b = new Decimal('0.2');
// console.log(a.plus(b).toString()); // "0.3" — exact, no floating point precision loss
// Native alternative for two-decimal money math without a dependency:
function addMoney(...centsValues) {
return centsValues.reduce((sum, cents) => sum + cents, 0);
}
console.log(addMoney(1999, 550, 333, 99) / 100); // 29.81, exact every time
9. Ansätze gegen Gleitkomma-Präzisionsfehler im Vergleich
Je nach Anwendungsfall unterscheidet sich, welcher Ansatz gegen Gleitkomma-Präzisionsfehler am besten passt, und die Wahl hängt stark davon ab, ob Ganzzahlen, einfache Geldbeträge oder komplexe Dezimalarithmetik im Vordergrund stehen.
| Anwendungsfall | Risiko | Empfohlener Ansatz | Grenzen |
|---|---|---|---|
| Zahlenvergleich | === schlägt fehl | Number.EPSILON-Toleranz | Schwellenwert je nach Größenordnung skalieren |
| Geldbeträge | Cent-Abweichungen bei Summen | Ganzzahl-Cent statt Fließkomma | Formatierung erst bei Anzeige |
| Große IDs | Präzisionsverlust über 2^53 | BigInt | Keine Nachkommastellen möglich |
| Komplexe Dezimalarithmetik | Kumulierte Rundungsfehler | decimal.js oder big.js | Zusätzlicher Rechenaufwand |
| Anzeige mit fixer Nachkommastelle | toFixed rundet manchmal falsch | Runden vor toFixed, EPSILON-Korrektur | String-Rückgabe explizit konvertieren |
Die Tabelle zeigt, dass es keine einzelne, universelle Lösung für Gleitkomma-Präzisionsfehler gibt, sondern die Wahl des Ansatzes vom konkreten Anwendungsfall abhängt. Wer Geldbeträge, große IDs und einfache numerische Vergleiche unterschiedlich behandelt, statt pauschal auf native Fließkommaarithmetik zu vertrauen, vermeidet die überwiegende Mehrheit der in der Praxis auftretenden Fehler.
Mironsoft
JavaScript-Debugging, Code-Reviews und Frontend-Architektur
Rundungsfehler bei euren Finanzberechnungen?
Wir prüfen bestehenden Code auf riskante Fließkomma-Arithmetik bei Geldbeträgen und IDs und migrieren Berechnungen auf Ganzzahl-Cent, BigInt oder dedizierte Dezimalarithmetik-Bibliotheken.
Code-Review
Gezielte Suche nach riskanten Fließkomma-Vergleichen und Summen
Finanz-Refactoring
Migration von Geldbeträgen auf Ganzzahl-Cent-Repräsentation
Bibliotheks-Auswahl
Auswahl und Integration von decimal.js oder big.js für Ihre Anforderungen
10. Zusammenfassung
Gleitkomma-Präzisionsfehler in JavaScript sind keine Eigenheit der Sprache, sondern eine unvermeidbare Konsequenz des IEEE-754-Standards, nach dem viele Dezimalbrüche nicht binär exakt darstellbar sind. 0.1 + 0.2 ergibt deshalb 0.30000000000000004, weil bereits die einzelnen Operanden gerundet gespeichert werden, bevor die Addition überhaupt stattfindet. Direkte Gleichheitsvergleiche mit === sind bei berechneten Fließkommazahlen deshalb grundsätzlich riskant und sollten durch einen Toleranzvergleich mit Number.EPSILON ersetzt werden.
Bei Geldbeträgen ist die robusteste Lösung, intern konsequent mit Ganzzahl-Cent statt mit der Hauptwährungseinheit zu rechnen, weil Ganzzahlen im sicheren Bereich exakt darstellbar sind. Für sehr große Ganzzahlen jenseits der sicheren Grenze schafft BigInt Abhilfe, für komplexe Dezimalarithmetik mit vielen Nachkommastellen bieten dedizierte Bibliotheken wie decimal.js die zuverlässigste Lösung. Wer diese drei Werkzeuge je nach Anwendungsfall gezielt einsetzt, umgeht die überwiegende Mehrheit der Gleitkomma-Präzisionsfehler in der Praxis.
Gleitkomma-Präzision in JavaScript, das Wichtigste auf einen Blick
Ursache
IEEE 754 kann viele Dezimalbrüche nicht binär exakt darstellen, dieselbe Ursache wie bei einem Drittel im Dezimalsystem.
Vergleiche
Niemals === für berechnete Fließkommazahlen. Toleranzvergleich mit Number.EPSILON verwenden.
Geld
Immer Ganzzahl-Cent intern speichern, erst bei Anzeige zur Hauptwährungseinheit formatieren.
Große Zahlen
BigInt für Ganzzahlen über 2^53, decimal.js oder big.js für komplexe Dezimalarithmetik.