3 Errores Comunes de JavaScript Explicados
Tres trampas de JavaScript explicadas: cálculos de coma flotante, comprobaciones de NaN y await en bucles, con sus soluciones.
0.1 + 0.2 no es igual a 0.3, NaN no es igual a sí mismo, y await dentro de un bucle puede convertir una página rápida en una lenta — tres comportamientos de JavaScript que parecen errores pero que en realidad son el lenguaje funcionando exactamente según lo que su especificación exige. Este artículo explica el mecanismo detrás de cada uno, no solo la extraña salida en la consola, y ofrece la solución correcta para cada caso. Todos ellos se manifiestan como síntomas reales que afectan al usuario: un total que difiere en un centavo, una validación que permite el paso de datos incorrectos, una pantalla que carga lentamente sin razón aparente. Entender el porqué es lo que te permite evitar los tres en producción y responderlos con precisión en una entrevista.
Puntos Clave
0.1 + 0.2devuelve0.30000000000000004porque los doubles IEEE 754 almacenan números en binario, y ni0.1ni0.2tienen una representación binaria finita exacta, por lo que cada uno se redondea antes de sumarse.- Para valores monetarios, calcula en centavos enteros en lugar de dólares en punto flotante; para comparaciones generales de flotantes, evalúa
Math.abs(a - b) < tolerancecon una tolerancia ajustada a la magnitud de tus números, no con unNumber.EPSILONaplicado indiscriminadamente. NaN === NaNesfalseporque la especificación IEEE 754 define NaN como desigual a todo valor, incluyéndose a sí mismo — usaNumber.isNaN(), que no realiza coerción, en lugar delisNaN()global.awaitdentro de un bucleforpausa el bucle completo en cada iteración; inicia las promesas primero y usaawait Promise.all(...)para ejecutar solicitudes independientes de forma concurrente.
Error #1: Por qué 0.1 + 0.2 no es 0.3 (aritmética de punto flotante)
Discover how at OpenReplay.com.
0.1 + 0.2 devuelve 0.30000000000000004 porque los números en JavaScript son flotantes de doble precisión IEEE 754 almacenados en base 2, y ni 0.1 ni 0.2 tienen una representación binaria finita exacta. Cada literal se redondea al valor de 64 bits representable más cercano en el momento en que se escribe, y la suma de esos dos valores ya redondeados se redondea a un número ligeramente superior a 0.3.
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
(0.1).toPrecision(20); // "0.10000000000000000555"
Esa última línea lo revela todo: el valor almacenado para 0.1 nunca fue exactamente 0.1. Esto no es una peculiaridad de JavaScript — es una propiedad del punto flotante de doble precisión compartida por Python, Java, C y cualquier lenguaje que utilice el mismo formato. El número 0.1 en binario es una fracción periódica, del mismo modo que 1/3 es 0.333… en decimal, por lo que debe truncarse.
La solución depende de lo que estés calculando:
// Dinero: trabajar en centavos enteros, formatear solo para mostrar
const total = 1010 + 2030; // 3040 centavos
(total / 100).toFixed(2); // "30.40"
// Comparación general: tolerancia ajustada a tu magnitud
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
Para valores monetarios, almacena y calcula en centavos enteros, nunca en dólares de punto flotante, y luego divide y aplica toFixed(2) en la capa de presentación. Para igualdad aproximada, compara contra una tolerancia. Number.EPSILON solo funciona para números alrededor de la magnitud de 1 — MDN advierte explícitamente que no es un umbral universal seguro, así que escala la tolerancia al tamaño de los valores que estás comparando. Si estás sumando un array, Math.sumPrecise() pasó a ser Baseline de disponibilidad reciente en abril de 2026, por lo que puede no ejecutarse en dispositivos más antiguos; además, ten en cuenta que aún no puede evitar el problema de precisión de 0.1 + 0.2 para literales individuales — solo previene la acumulación de errores a lo largo de una suma extensa.
Un total que difiere en un centavo solo se reproduce con la secuencia exacta de valores que se sumaron, razón por la cual una repetición de sesión que reconstruye el orden real de entrada es más útil aquí que una captura de pantalla del número incorrecto.
Error #2: NaN es el único valor que no es igual a sí mismo
NaN === NaN es false porque el estándar IEEE 754 define NaN (“Not-a-Number”) como desigual a todo valor, incluyéndose a sí mismo, y JavaScript sigue esa regla literalmente. Esto convierte a NaN en el único valor del lenguaje que no es igual a sí mismo — un hecho que incluso puedes usar como detector de NaN.
NaN === NaN; // false
NaN == NaN; // false
typeof NaN; // "number"
typeof NaN es 'number': NaN es un valor numérico que representa un resultado numérico indefinido o irrepresentable, no un tipo separado. La trampa real es el isNaN() global, que coerciona su argumento a número antes de evaluar, por lo que valores que no son NaN en absoluto se reportan como si hubieran sido verificados como números:
isNaN(''); // false — '' se coerciona a 0
isNaN([]); // false — [] se coerciona a 0
isNaN('45'); // false — '45' se coerciona a 45
isNaN({}); // true — {} se coerciona a NaN
La solución es Number.isNaN(), añadido en ES2015, que no realiza coerción y devuelve true únicamente para el valor NaN real:
| Entrada | isNaN() (con coerción) | Number.isNaN() (sin coerción) |
|---|---|---|
NaN | true | true |
'NaN' | true | false |
'' | false | false |
[] | false | false |
'45' | false | false |
undefined | true | false |
Usa Number.isNaN() para verificar “¿es este específicamente el valor NaN?”, y Number.isFinite() cuando quieras saber “¿es este un número real y finito?” — rechaza NaN, Infinity y no-números sin coerción. Un mito relacionado que vale la pena desmentir: parseInt("032") devuelve 32, no 26. La detección automática de cadenas con cero inicial como octales fue eliminada en ECMAScript 5, por lo que la afirmación del 26 ampliamente copiada es una reliquia anterior a 2011 — sigue pasando un radix, pero por la razón correcta.
Cuando una validación falla porque isNaN('') coerciona una cadena vacía a 0, el valor problemático a menudo parece “vacío” en un reporte de error; reproducir las pulsaciones de teclas reales y el estado del campo muestra exactamente qué se coló.
Error #3: await en un bucle serializa las solicitudes y ralentiza tu interfaz
await dentro de un bucle for pausa el bucle completo en cada iteración, por lo que las solicitudes que no tienen dependencia entre sí se ejecutan estrictamente una tras otra en lugar de en paralelo. Cada iteración espera a que su promesa se resuelva antes de que siquiera comience la siguiente solicitud, convirtiendo N viajes de ida y vuelta independientes en N secuenciales.
// Serial: cada await bloquea la siguiente iteración
async function getUsers(ids) {
const users = [];
for (const id of ids) {
users.push(await fetchUser(id)); // espera ~1.5s, cada vez
}
return users;
}
La solución es iniciar todas las promesas primero y luego esperarlas juntas con Promise.all(), que lanza las solicitudes de forma concurrente y se resuelve una vez que todas han completado:
async function getUsers(ids) {
return Promise.all(ids.map(fetchUser));
}
Con una latencia fija simulada de 1.5 segundos por solicitud (ilustrando la serialización, no tiempos de red reales), la diferencia escala con el tamaño del lote:
| Enfoque | 3 solicitudes | 10 solicitudes |
|---|---|---|
await en un bucle (serial) | ~4.5s | ~15s |
Promise.all (concurrente) | ~1.5s | ~1.5s |
Una advertencia: Promise.all rechaza en cuanto cualquier promesa individual rechaza, descartando el resto. Cuando un fallo no debería abortar el lote completo, usa Promise.allSettled() en su lugar, que espera a todas las promesas independientemente del resultado y reporta cada una como fulfilled o rejected. Promise.allSettled se introdujo en ES2020 y es compatible de forma nativa en todos los navegadores modernos desde principios de 2023, por lo que no requiere polyfill en entornos actuales. Serializa deliberadamente solo cuando cada solicitud depende genuinamente del resultado de la anterior.
Una pantalla que está “simplemente lenta” para algunos usuarios es difícil de detectar en una revisión de código; una repetición de sesión muestra las solicitudes disparándose una tras otra en lugar de simultáneamente, lo que apunta directamente a un bucle con await.
Próximos pasos
Estos tres comportamientos son correctos según la especificación, que es exactamente por qué sobreviven la revisión de código y llegan a los usuarios: los flotantes binarios se redondean porque IEEE 754 así lo establece, NaN se rechaza a sí mismo porque el estándar lo define de esa manera, y await serializa porque eso es lo que significa pausar la ejecución. Las defensas son pequeñas y mecánicas — centavos enteros y tolerancias escaladas por magnitud para flotantes, Number.isNaN() y Number.isFinite() para verificaciones numéricas, Promise.all o Promise.allSettled para trabajo asíncrono independiente. Audita tu propio código de cálculo monetario, guardas de validación y bucles de obtención de datos contra estos tres patrones, y la clase de errores “imposibles” que generan dejará de llegar a producción.
Preguntas Frecuentes
¿Cuál es la diferencia entre el isNaN() global y Number.isNaN()?
El isNaN() global coerciona su argumento a número antes de evaluar, por lo que isNaN('') e isNaN([]) devuelven false porque '' y [] se coercionan a 0, mientras que isNaN('NaN') devuelve true porque 'NaN' se coerciona al valor NaN. Number.isNaN(), añadido en ES2015, no realiza coerción y devuelve true únicamente para el valor NaN real. Usa Number.isNaN() cuando necesites detectar NaN específicamente.
¿El problema de punto flotante con 0.1 + 0.2 ocurre solo en JavaScript?
No. Ocurre en cualquier lenguaje que utilice flotantes de doble precisión IEEE 754, incluyendo Python, Java, C, C++ y Ruby. Ni 0.1 ni 0.2 tienen una representación binaria finita exacta, por lo que cada uno se redondea al almacenarse, y la suma se redondea a 0.30000000000000004. Es una propiedad del formato de punto flotante binario en sí mismo, no un error de JavaScript, por lo que las mismas soluciones basadas en centavos enteros y tolerancias se aplican en todos los lenguajes.
¿Cuándo debería usar Promise.allSettled en lugar de Promise.all?
Usa Promise.allSettled cuando un fallo no deba abortar el lote completo. Promise.all rechaza en cuanto cualquier promesa individual rechaza y descarta los resultados de las demás, por lo que es adecuado para operaciones de todo o nada. Promise.allSettled espera a todas las promesas independientemente del resultado y devuelve un array que reporta cada una como fulfilled o rejected, que es lo que necesitas cuando obtienes múltiples recursos independientes y el éxito parcial es aceptable. Se introdujo en ES2020 y no requiere polyfill en entornos modernos.
¿Es correcto usar await dentro de un bucle en algún caso?
Sí, cuando cada iteración depende genuinamente del resultado de la anterior, como al paginar a través de una API donde la siguiente solicitud necesita un cursor devuelto por la última respuesta, o al limitar deliberadamente la velocidad para no sobrecargar un servidor. En esos casos, la ejecución serial es el comportamiento deseado. El problema solo aplica a solicitudes independientes que no tienen dependencia entre sí, donde usar await en un bucle serializa innecesariamente trabajo que Promise.all podría ejecutar de forma concurrente.
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