Errores de precisión de punto flotante: Por qué 0.1 + 0.2 ≠ 0.3

9 min24 de mayo de 2026

El error de precisión de punto flotante que todos encuentran

Abre cualquier lenguaje de programación y escribe 0.1 + 0.2. Obtendrás 0.30000000000000004. No 0.3. Esto no es un bug en JavaScript, Python, Java, C++ o Rust — es una consecuencia fundamental de cómo las computadoras representan fracciones decimales en binario. Los errores de precisión de punto flotante que ves no son errores; son el resultado matemáticamente correcto de aritmética binaria sobre valores aproximados.

La razón: 0.1 en decimal es una fracción repetitiva en binario (0.0001100110011...), igual que 1/3 es repetitivo en decimal (0.333...). Un float de 64 bits solo puede almacenar 53 bits significativos, así que 0.1 se redondea al valor representable más cercano: 0.1000000000000000055511151231257827021181583404541015625. Cuando sumas dos de estas aproximaciones, los errores de redondeo no se cancelan — se acumulan.

Esto afecta a todos los lenguajes que usan punto flotante IEEE 754 (que son todos para matemáticas aceleradas por hardware). Python, JavaScript, C, Java, Go, Rust — todos producen el mismo resultado porque todos usan el mismo formato de doble precisión de 64 bits. Los únicos lenguajes que evitan esto por defecto son los que usan aritmética de precisión arbitraria (como el tipo Rational de Haskell o el módulo Decimal de Python), que intercambian velocidad por exactitud.

Cómo IEEE 754 representa números

Los floats de doble precisión (64 bits) IEEE 754 almacenan números como: 1 bit de signo, 11 bits de exponente y 52 bits de mantisa (más 1 bit implícito inicial = 53 bits de precisión). El valor es: (-1)^signo × 2^(exponente-1023) × 1.mantisa. Esto te da aproximadamente 15-17 dígitos decimales significativos de precisión y un rango de ±5×10^-324 a ±1.8×10^308.

Los 53 bits de mantisa significan que puedes representar enteros exactamente hasta 2^53 (9,007,199,254,740,992). Más allá de eso, los números representables consecutivos están separados por más de 1. En JavaScript: 9007199254740992 + 1 === 9007199254740992 evalúa a true. El número literalmente no puede representarse. Por eso JavaScript tiene BigInt y por qué los IDs de base de datos mayores a 2^53 deben transmitirse como strings en JSON.

Entre cualquier par de floats representables, hay infinitos números reales que no pueden representarse. El espacio entre números representables varía — cerca de cero, los floats consecutivos están separados por ~5×10^-324. Cerca de 1.0, están separados por ~1.1×10^-16. Cerca de 1,000,000, están separados por ~1.2×10^-10. Esto significa que la precisión relativa es aproximadamente constante (~15 dígitos), pero la precisión absoluta disminuye conforme los números crecen.

Valores especiales: +0 y -0 (sí, el cero negativo existe y -0 === +0 en JavaScript), Infinity y -Infinity (resultado de overflow o división por cero), y NaN (Not a Number — resultado de 0/0, sqrt(-1) u operaciones inválidas). NaN tiene la propiedad bizarra de que NaN !== NaN — es el único valor en IEEE 754 que no es igual a sí mismo. Usa Number.isNaN() o isNaN() para verificarlo.

// The classic floating point precision errors
0.1 + 0.2                    // 0.30000000000000004
0.1 + 0.2 === 0.3            // false
1.0 - 0.9                    // 0.09999999999999998
0.1 * 0.2                    // 0.020000000000000004

// Large integer precision loss
9007199254740992 + 1         // 9007199254740992 (unchanged!)
9007199254740993 === 9007199254740992  // true (!)

// Special values
1 / 0                        // Infinity
-1 / 0                       // -Infinity
0 / 0                        // NaN
NaN === NaN                  // false (!)
Number.isNaN(NaN)            // true

// The epsilon approach for comparison
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON  // true
// But EPSILON is too small for accumulated errors:
Math.abs(0.1 + 0.2 + 0.3 - 0.6) < Number.EPSILON  // false!

Punto flotante y dinero (no uses floats)

Nunca uses punto flotante para cálculos financieros. $0.10 + $0.20 debería ser igual a $0.30, no $0.30000000000000004. A lo largo de miles de transacciones, estos errores diminutos se acumulan en discrepancias reales. Un banco que procesa 10 millones de transacciones por día con errores de redondeo de ±0.000000000000001 por transacción podría desviarse $0.01 por día — lo que dispara fallas de auditoría y problemas regulatorios.

La solución estándar: almacena dinero como enteros en la unidad más pequeña (centavos). $19.99 se convierte en 1999 centavos. Toda la aritmética es matemática de enteros exacta. Solo convierte a decimal para mostrar. Así es como funcionan Stripe, PayPal y todo sistema de pagos serio. En JavaScript: const total = priceInCents * quantity; const display = (total / 100).toFixed(2).

Para monedas con precisión sub-centavo (criptomonedas, forex), usa una librería de punto fijo. JavaScript no tiene un tipo decimal nativo, pero librerías como decimal.js, big.js o dinero.js manejan aritmética decimal de precisión arbitraria. Python tiene el módulo decimal (from decimal import Decimal; Decimal("0.1") + Decimal("0.2") == Decimal("0.3") es True). Java tiene BigDecimal. Usa estos para cualquier cálculo donde los resultados decimales exactos importen.

Un ejemplo del mundo real: en 2012, Knight Capital perdió $440 millones en 45 minutos debido a un bug de software. Aunque no fue estrictamente un error de punto flotante, ilustra cómo pequeños errores computacionales en sistemas financieros se propagan catastróficamente. La lección: el código financiero necesita aritmética exacta, pruebas extensivas y mecanismos de seguridad. Nuestra herramienta percentage-calculator usa aritmética de enteros internamente para evitar estos problemas al calcular tasas de impuestos y descuentos.

Comparar números de punto flotante

Nunca uses === (o ==) para comparar resultados de punto flotante. En su lugar, verifica si la diferencia es menor que una tolerancia: Math.abs(a - b) < epsilon. Pero ¿cuánto debería ser epsilon? Number.EPSILON (2.2×10^-16) es la diferencia más pequeña entre 1.0 y el siguiente float representable — es apropiado para comparar números cerca de 1.0 pero demasiado pequeño para números más grandes y demasiado grande para números cerca de cero.

Un mejor enfoque: epsilon relativo. Compara la diferencia con la magnitud de los números: Math.abs(a - b) <= epsilon * Math.max(Math.abs(a), Math.abs(b)). Esto escala la tolerancia con el tamaño de los valores. Para la mayoría de aplicaciones, un epsilon relativo de 1e-9 a 1e-12 funciona bien. Para cálculos acumulados (sumar miles de valores), podrías necesitar 1e-6.

El enfoque ULP (Unit in the Last Place): dos floats son "iguales" si difieren en como máximo N ULPs. Un ULP es la distancia entre un float y su vecino más cercano. Esta es la comparación más rigurosa matemáticamente pero más difícil de implementar. En la práctica, la comparación con epsilon relativo cubre el 99% de los casos de uso.

Para ordenamiento y deduplicación, la comparación de punto flotante es especialmente complicada. Si estás eliminando valores "duplicados" de una lista de mediciones, necesitas una relación de equivalencia consistente (si a≈b y b≈c, entonces a≈c). La comparación simple con epsilon no garantiza esta transitividad. Considera redondear a un número fijo de dígitos significativos antes de comparar, o usar representaciones enteras.

Computación científica: Cuando la precisión importa más

Cancelación catastrófica: restar dos números casi iguales pierde la mayoría de los dígitos significativos. Si a = 1.0000001 y b = 1.0000000 (ambos almacenados con 15 dígitos de precisión), a - b = 0.0000001 tiene solo 1 dígito de precisión — los otros 14 dígitos son ruido. Esto ocurre en cálculos de fórmula cuadrática, derivadas numéricas y cualquier algoritmo que calcule diferencias pequeñas de números grandes.

La solución para la fórmula cuadrática: en vez de x = (-b ± sqrt(b²-4ac)) / 2a, usa la forma numéricamente estable. Calcula una raíz con la fórmula estándar (eligiendo el signo que evita la cancelación), luego usa x1 * x2 = c/a para obtener la otra raíz. Esto evita la cancelación catastrófica que ocurre cuando b² >> 4ac. Nuestra herramienta scientific-calculator usa esta forma estabilizada internamente.

El orden de la suma importa. Sumar 1e-16 a 1.0 da 1.0 (el valor pequeño está por debajo del umbral de precisión). Pero sumar 1e-16 un billón de veces debería dar 0.0001. La suma ingenua pierde estas contribuciones pequeñas por completo. La suma de Kahan (también llamada suma compensada) rastrea el error de redondeo y lo corrige, dando resultados precisos a la precisión completa del float. Úsala cuando sumes muchos valores de magnitud variable.

Aritmética de intervalos: en vez de calcular un solo float, rastrea el rango [inferior, superior] que contiene el resultado verdadero. Después de cada operación, el intervalo se amplía para considerar el redondeo. Si tu intervalo final es [2.99999, 3.00001], sabes que la respuesta verdadera es 3.0 ± 0.00001. Esto te da un límite de error riguroso, a diferencia de la computación con un solo float donde estás adivinando cuánto error se acumuló. Librerías como MPFI (C) y pyinterval (Python) implementan esto.

Particularidades de cada lenguaje

JavaScript: Todos los números son doubles de 64 bits (no había tipo entero hasta BigInt). Esto significa que la aritmética de enteros es exacta solo hasta 2^53. JSON.parse() de enteros grandes pierde precisión silenciosamente: JSON.parse("9007199254740993") devuelve 9007199254740992. Las APIs que devuelven IDs grandes (Twitter, Discord) deben enviarlos como strings. Usa BigInt para aritmética de enteros exacta más allá de 2^53.

Python: float es double de 64 bits. Pero Python también tiene Decimal (decimal de precisión arbitraria) y Fraction (aritmética racional exacta). Para código financiero: from decimal import Decimal, ROUND_HALF_UP. Para código científico: numpy usa doubles de 64 bits por defecto pero soporta float128 en algunas plataformas. El operador // de Python hace división entera (floor), que interactúa con números negativos de forma diferente a la truncación.

C/C++: float es 32 bits (7 dígitos de precisión), double es 64 bits (15 dígitos), long double es 80 o 128 bits dependiendo de la plataforma. La FPU x87 usa precisión extendida de 80 bits internamente, lo que significa que el mismo código puede dar resultados diferentes dependiendo de si los valores intermedios se quedan en registros (80 bits) o se almacenan en memoria (64 bits). Usa -ffp-contract=off y -fno-fast-math para resultados reproducibles.

SQL: FLOAT y DOUBLE son tipos IEEE 754 con los mismos problemas de precisión. DECIMAL(p,s) y NUMERIC(p,s) son tipos de punto fijo exactos — usa estos para dinero. El NUMERIC de PostgreSQL puede almacenar hasta 131,072 dígitos antes del punto decimal y 16,383 después. El DECIMAL de MySQL soporta hasta 65 dígitos en total. Siempre usa DECIMAL para columnas financieras, nunca FLOAT o DOUBLE.

Reglas prácticas para evitar bugs de punto flotante

Regla 1: Usa enteros para dinero. Almacena centavos, no pesos. Almacena satoshis, no bitcoin. Convierte a decimal solo para mostrar. Esto elimina el 90% de los bugs de punto flotante en aplicaciones de negocio.

Regla 2: Nunca compares floats con ===. Usa una comparación basada en tolerancia apropiada para tu dominio. Para coordenadas de UI, 0.01 píxeles de tolerancia está bien. Para cálculos científicos, usa epsilon relativo. Para cálculos financieros, no deberías estar usando floats en absoluto (ver Regla 1).

Regla 3: Desconfía de la resta. Si estás restando dos números que podrían ser cercanos en valor, estás en riesgo de cancelación catastrófica. Reformula el algoritmo para evitar la resta, o usa mayor precisión para ese cálculo específico. La fórmula cuadrática, las derivadas numéricas y los cálculos de varianza son ejemplos clásicos.

Regla 4: Prueba con entradas adversariales. Valores cerca de cero, valores muy grandes, valores que son potencias exactas de 2 (representables exactamente) vs valores que no lo son (0.1, 0.3, 0.7). Sumas de muchos valores pequeños. Diferencias de valores casi iguales. Si tu código maneja dinero, prueba con $0.01, $0.10, $99.99 y $999,999,999.99. Nuestra herramienta basic-calculator está diseñada para manejar estos casos borde correctamente usando representaciones internas apropiadas.