JavaScript tiene fama de hacer cosas absurdas: sumar dos decimales y equivocarse, ordenar mal una lista de números o decir que «no es un número» es un número. Aquí están las rarezas más famosas, ejecutadas de verdad, con la explicación de por qué pasan y la costumbre que las evita.
Todo lo que hay en esta página se ejecutó con Node.js 24 y la salida es la real. Ninguna de estas rarezas es un fallo: todas están escritas en la especificación del lenguaje, y casi todas vienen de decisiones tomadas en 1995 que ya no se pueden cambiar sin romper la web. Saber por qué pasan es lo que distingue a quien programa en JavaScript de quien pelea con él.
- 0.1 + 0.2 no da 0.3
- sort() ordena los números como si fueran texto
- map(parseInt) devuelve NaN
- NaN no es igual a sí mismo
- typeof null es «object»
- [] + {} y otras sumas imposibles
- null >= 0 es verdadero, pero null == 0 no
- Un return que devuelve undefined
- Math.max() es menor que Math.min()
- Arrays con huecos
- El bucle que imprime siempre 3
- this se pierde
- Los meses empiezan en cero
- Un emoji mide 2
- toFixed y Math.round redondean raro
- Existe el -0
- JSON.stringify se calla cosas
- forEach no espera a async
0.1 + 0.2 no da 0.3
console.log(0.1 + 0.2); console.log(0.1 + 0.2 === 0.3); console.log((0.1).toFixed(20));
salida0.30000000000000004 false 0.10000000000000000555
Por qué: los números se guardan en binario, y 0.1 en binario tiene infinitas cifras, igual que 1/3 en decimal (0.3333…). Se guarda el número representable más cercano, que es un poco mayor que 0.1. Al sumar dos aproximaciones, el error se nota. No es cosa de JavaScript: Python, Java, C y Excel dan lo mismo, porque todos usan el estándar IEEE 754.
===; usa una tolerancia, Math.abs(a - b) < Number.EPSILON, o redondea al mostrar con toFixed. Para dinero, trabaja en centavos con enteros: (10 + 20) / 100 da 0.3 exacto.sort() ordena los números como si fueran texto
console.log([10, 1, 3, 25, 2].sort()); console.log([10, 1, 3, 25, 2].sort((a, b) => a - b));
salida[ 1, 10, 2, 25, 3 ] [ 1, 2, 3, 10, 25 ]
Por qué: sin argumentos, sort convierte cada elemento en texto y los ordena como en un diccionario. «10» empieza por «1», que va antes que «2», así que 10 queda antes que 2. Tiene sentido para un array que puede mezclar cualquier cosa, pero sorprende con números.
Con texto pasa algo parecido: el orden es el de los códigos Unicode, así que las mayúsculas van antes que las minúsculas y las letras con tilde, al final.
const nombres = ["ñu", "Zoe", "árbol", "oso"]; console.log([...nombres].sort()); console.log([...nombres].sort((a, b) => a.localeCompare(b, "es")));
salida[ 'Zoe', 'oso', 'árbol', 'ñu' ] [ 'árbol', 'ñu', 'oso', 'Zoe' ]
(a, b) => a - b para números y a.localeCompare(b, "es") para texto en español. Y recuerda que sort modifica el array original; si no quieres eso, usa toSorted, que devuelve una copia ordenada.map(parseInt) devuelve NaN
console.log(["1", "7", "11"].map(parseInt)); console.log(["1", "7", "11"].map(Number));
salida[ 1, NaN, 3 ] [ 1, 7, 11 ]
Por qué: map llama a la función con tres argumentos: el elemento, su posición y el array. parseInt acepta dos: el texto y la base en la que está escrito. Así que la posición acaba haciendo de base.
map(Number), o escribe la flecha completa con la base: map((s) => parseInt(s, 10)). Pasar una función tal cual a map solo es seguro si sabes cuántos argumentos acepta.NaN no es igual a sí mismo
console.log(typeof NaN);
console.log(NaN === NaN);
console.log([NaN].indexOf(NaN), [NaN].includes(NaN));
console.log(Number.isNaN("hola"), isNaN("hola"));salidanumber false -1 true false true
Por qué: NaN significa «no es un número», pero es un valor del tipo number: el resultado de una operación numérica que no tiene sentido. El estándar IEEE 754 dice que NaN no es igual a nada, ni a sí mismo, porque dos operaciones fallidas no tienen por qué dar «lo mismo». Por eso indexOf, que usa ===, no lo encuentra, e includes, que usa otra comparación, sí.
Number.isNaN(x). No uses el isNaN global, que convierte antes a número y por eso dice que "hola" es NaN.typeof null es «object»
console.log(typeof null);
salidaobject
Por qué: es un error de la primera versión de JavaScript, escrita en diez días en 1995. Los valores llevaban una etiqueta de tipo en sus bits bajos, la de los objetos era 0, y null se representaba como el puntero nulo, que es todo ceros. Años después se propuso corregirlo en una nueva versión del estándar, y se descartó porque rompía demasiadas páginas.
null, compáralo directamente: x === null. Y si lo que quieres saber es si es un objeto de verdad, x !== null && typeof x === "object".[] + {} y otras sumas imposibles
console.log([] + []);
console.log([] + {});
console.log([1, 2] + [3]);
console.log("b" + "a" + +"a" + "a");
console.log("5" - 2, "5" + 2, true + true);salida[object Object] 1,23 baNaNa 3 52 2
La primera línea imprime una cadena vacía. Por qué: el + primero convierte los objetos y los arrays en valores simples. Un array se convierte en sus elementos separados por comas ([] queda en "" y [1, 2] en "1,2"), y un objeto en "[object Object]". Después, si alguno de los dos es texto, concatena; si no, suma como números.
El «baNaNa» sale del +"a" del medio: un + delante de un valor lo convierte en número, y "a" como número es NaN, que después se concatena como texto. Y si has visto que {} + [] da 0 en la consola, también es verdad, pero por otro motivo: al principio de una línea, {} se lee como un bloque de código vacío, no como un objeto, y lo que queda es +[], que es 0. Lo comprobamos con eval("{} + []"), que da 0, frente a eval("({} + [])"), que da "[object Object]".
Number(...) o String(...), y para unir texto usa plantillas: `${a} ${b}`. TypeScript marca la mayoría de estos casos como error.null >= 0 es verdadero, pero null == 0 no
console.log(null > 0, null == 0, null >= 0); console.log(undefined > 0, undefined == 0, undefined >= 0); console.log([] == ![]);
salidafalse false true false false false true
Por qué: los operadores > y >= convierten null en número, que es 0, así que null >= 0 es 0 >= 0. Pero == tiene reglas propias: null solo es igual a null y a undefined, y a nada más. Y undefined como número es NaN, que pierde en todas las comparaciones. El [] == ![] es la misma historia: ![] es false (un array es verdadero), y al comparar con == los dos lados acaban convertidos en 0.
===, y no compares con < o > valores que puedan ser null o undefined sin comprobarlos antes. Lo explicamos en la lección 2.Un return que devuelve undefined
function crearUsuario() {
return
{
nombre: "Ana"
};
}
console.log(crearUsuario());salidaundefined
Por qué: JavaScript inserta puntos y comas automáticamente, y una de sus reglas dice que tras return seguido de un salto de línea va un punto y coma. La función devuelve undefined y lo de abajo es un bloque de código que nunca se ejecuta. Lo contrario también pasa: si una línea empieza por [ o (, se pega a la anterior.
const a = 1 const b = a [1, 2].forEach((n) => console.log(n))
salidaTypeError: Cannot read properties of undefined (reading 'forEach')
Se ha leído como const b = a[1, 2].forEach(...): la propiedad 2 del número 1, que no existe.
return (return {), y pon los puntos y comas, o deja que los ponga un formateador como Prettier.Math.max() es menor que Math.min()
console.log(Math.max(), Math.min()); console.log(Math.max(...[]));
salida-Infinity Infinity -Infinity
Por qué: tiene lógica matemática. Math.max empieza con el valor más pequeño posible y lo va superando con cada argumento; sin argumentos, devuelve ese valor inicial. Es el mismo motivo por el que una suma vacía vale 0 y un producto vacío vale 1.
Math.max(...lista), o recibirás -Infinity como si fuera un dato.Arrays con huecos
console.log(new Array(3));
console.log(new Array(3, 4));
console.log(new Array(3).map((_, i) => i));
console.log(Array.from({ length: 3 }, (_, i) => i));
const b = [1, 2, 3];
delete b[1];
console.log(b, b.length);salida[ <3 empty items> ] [ 3, 4 ] [ <3 empty items> ] [ 0, 1, 2 ] [ 1, <1 empty item>, 3 ] 3
Por qué: con un solo número, new Array(3) crea un array de longitud 3 sin elementos; con dos, crea un array con esos dos elementos. Los huecos no son undefined: no existen, y por eso map se los salta. delete hace lo mismo: deja un hueco y no cambia la longitud.
Array.from({ length: n }, ...) o new Array(n).fill(0). Para quitar un elemento, splice o filter, nunca delete.El bucle que imprime siempre 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}salidavar: 3 var: 3 var: 3 let: 0 let: 1 let: 2
Por qué: las funciones de setTimeout se ejecutan cuando el bucle ya ha terminado. Con var hay una sola variable i para todo el bucle, y cuando las funciones la leen ya vale 3. Con let, cada vuelta tiene su propia variable, y cada función recuerda la suya. Esto se explica a fondo en la lección de funciones y closures.
var. Con let y const este problema desaparece.this se pierde
const contador = {
cuenta: 0,
sumar() {
this.cuenta++;
return this.cuenta;
},
};
console.log(contador.sumar());
const suelta = contador.sumar;
suelta();salida1
salidaTypeError: Cannot read properties of undefined (reading 'cuenta')
Por qué: en JavaScript, this no depende de dónde se escribió la función, sino de cómo se llama. contador.sumar() tiene un objeto delante del punto, y ese es this. suelta() no tiene nada delante, así que this es undefined. Pasa a menudo sin darse cuenta, al pasar un método como callback: setTimeout(contador.sumar, 100) o boton.addEventListener("click", objeto.metodo).
() => contador.sumar(), o átalo con contador.sumar.bind(contador). Las reglas completas están en la lección de clases y this.Los meses empiezan en cero
const d = new Date(2026, 9, 19);
console.log(d.getMonth(), d.toDateString());
console.log(new Date(2026, 1, 31).toDateString());
console.log(new Date("2026-10-19").getDate(), new Date("2026-10-19T00:00").getDate());salida (en México, UTC-6)9 Mon Oct 19 2026 Tue Mar 03 2026 18 19
Tres sorpresas en tres líneas. Los meses van de 0 a 11, así que octubre es el 9; los días, en cambio, empiezan en 1. Una fecha imposible no da error, se desborda: el 31 de febrero se convierte en el 3 de marzo. Y una fecha escrita solo con el día, "2026-10-19", se interpreta como medianoche en UTC, que en México todavía es el día 18; si añades la hora, se interpreta en la hora local. Todo se copió de Java 1.0, que tenía los mismos defectos y los corrigió hace años.
Intl.DateTimeFormat o toLocaleDateString("es-MX"). Para cálculos serios, una biblioteca o Temporal, la nueva API de fechas que corrige todo esto, cuando esté disponible en todas partes: Node 24, por ejemplo, todavía no la trae. Más en el ejemplo de fechas.Un emoji mide 2
console.log("👍".length, [..."👍"].length);
const familia = "👨👩👧";
console.log(familia.length, [...familia].length);
const s = new Intl.Segmenter("es", { granularity: "grapheme" });
console.log([...s.segment(familia)].length);salida2 1 8 5 1
Por qué: las cadenas de JavaScript cuentan unidades de UTF-16, no letras. Los emojis no caben en una unidad y ocupan dos. Recorrer la cadena con [...s] cuenta caracteres Unicode, pero el emoji de la familia está hecho de tres personas unidas por caracteres invisibles: cinco caracteres que se ven como uno. Solo Intl.Segmenter cuenta lo que una persona llamaría «un carácter».
Intl.Segmenter, y no cortes cadenas con slice por posiciones arbitrarias: puedes partir un emoji por la mitad.toFixed y Math.round redondean raro
console.log(1.005.toFixed(2)); console.log((0.5).toFixed(0), (1.5).toFixed(0), (2.5).toFixed(0)); console.log(Math.round(2.5), Math.round(-2.5));
salida1.00 1 2 3 3 -2
Por qué: 1.005 se guarda como 1.00499999999999989..., igual que le pasaba a 0.1, y redondeado a dos decimales da 1.00. Y Math.round redondea los .5 siempre hacia arriba, hacia el infinito positivo: 2.5 sube a 3, pero -2.5 sube a -2. No es el redondeo «alejándose del cero» que se enseña en el colegio, ni el del banquero que usa Python.
Intl.NumberFormat. Si necesitas un redondeo concreto con negativos, escríbelo tú: Math.sign(x) * Math.round(Math.abs(x)).Existe el -0
console.log(0 === -0, Object.is(0, -0)); console.log(1 / 0, 1 / -0); console.log(String(-0), JSON.stringify(-0));
salidatrue false Infinity -Infinity 0 0
Por qué: en IEEE 754 el signo es un bit aparte, así que hay un cero positivo y uno negativo. Aparece, por ejemplo, al redondear -0.4. === los considera iguales y al convertirlos a texto los dos son "0", pero se comportan distinto al dividir. Object.is es la comparación que sí los distingue (y la que dice que NaN es igual a NaN).
JSON.stringify se calla cosas
console.log(JSON.stringify({ a: undefined, b: () => 1, c: NaN, d: new Date(0), e: Infinity }));
console.log(JSON.stringify([undefined, () => 1]));salida{"c":null,"d":"1970-01-01T00:00:00.000Z","e":null}
[null,null]Por qué: JSON es un formato más pobre que JavaScript: no tiene undefined, ni funciones, ni NaN, ni Infinity, ni fechas. En un objeto, las propiedades con undefined o funciones desaparecen; en un array se convierten en null; NaN e Infinity pasan a null, y las fechas a texto, que al leerlas de vuelta con JSON.parse siguen siendo texto. Y un BigInt directamente da error.
new Date(texto). Más en el ejemplo de JSON.forEach no espera a async
const espera = (ms) => new Promise((ok) => setTimeout(ok, ms));
const lista = [3, 1, 2];
lista.forEach(async (n) => {
await espera(n * 10);
console.log("terminó", n);
});
console.log("después del forEach");salidadespués del forEach terminó 1 terminó 2 terminó 3
Por qué: forEach llama a la función por cada elemento y no mira lo que devuelve. Una función async devuelve una promesa al instante, así que forEach termina sin esperar a ninguna, el código de después se ejecuta primero, y las tareas acaban en el orden en el que terminan, no en el de la lista. Lo comprobamos diez veces y el orden fue siempre el mismo.
for (const n of lista) { await ... }; para lanzarlas todas a la vez y esperar a que terminen, await Promise.all(lista.map(async (n) => ...)). Está explicado en la lección de asincronía.Resumen: la regla detrás de cada rareza
| Rareza | La causa | La costumbre que la evita |
|---|---|---|
0.1 + 0.2, toFixed | Los decimales se guardan en binario | Tolerancia al comparar, enteros para el dinero |
sort() | Sin comparador, compara texto | Pasar siempre un comparador |
map(parseInt) | map pasa más argumentos de los que crees | Escribir la flecha completa |
NaN, -0 | IEEE 754 | Number.isNaN, Object.is |
[] + {}, == | Conversiones automáticas | === y convertir a mano |
return + salto | Punto y coma automático | Llave en la misma línea, formateador |
Huecos, var | Diseño antiguo | Array.from, let y const |
this | Depende de cómo se llama la función | Flechas o bind |
| Fechas | Copiado de Java 1.0 | Intl y, pronto, Temporal |
| Emojis | Las cadenas cuentan unidades UTF-16 | Intl.Segmenter |
forEach + async | forEach no espera promesas | for...of o Promise.all |
¿Quieres comprobar si te las sabes? Varias aparecen en el test de JavaScript.
Todo el código de esta página se ejecutó con Node.js 24.22 en un contenedor limpio; las salidas y los errores están copiados de esa ejecución.
