Diez hábitos recurrentes de JavaScript que socavan silenciosamente tu base de código
Explica diez errores comunes en JavaScript y TypeScript, desde la igualdad laxa hasta la mutación de estado, y muestra patrones más seguros para reemplazar cada uno.
Casi cualquier proyecto en JavaScript que abordes, independientemente de la empresa o del framework utilizado, suele presentar el mismo conjunto reducido de problemas recurrentes. Rara vez se trata de errores exóticos o casos límite impredecibles; son los mismos hábitos que aparecen una y otra vez.
Ninguno de estos problemas romperá tu aplicación de inmediato. Precisamente por eso son tan riesgosos. Permanecen latentes hasta que el proyecto crece, un nuevo miembro del equipo comienza a modificar el código o el uso aumenta repentinamente, y solo entonces surgen como aquel tipo de errores que pueden consumir toda una tarde. A continuación se presentan los diez patrones que aparecen con mayor frecuencia, junto con alternativas mejores.
1. Uso de == en lugar de ===
El operador de igualdad laxa en JavaScript fuerza la conversión de tipos antes de compararlos, y los resultados son notoriamente difíciles de predecir:
0 == "0" // true
0 == "" // true
"" == "0" // false
null == undefined // true
Claro, existe una lógica interna en estas reglas de coerción una vez que las has memorizado. Pero nadie debería tener que mantener ese modelo mental activo solo para escribir una condición simple. Usa siempre ===; este operador verifica tanto el valor como el tipo al mismo tiempo, brindándote exactamente la comparación que pretendías sin conversiones ocultas.
if (userInput === "0") { ... } // clear, predictable
2. Mutar el estado directamente
Este error genera bugs que son especialmente difíciles de rastrear, ya que los síntomas suelen aparecer lejos de la causa real:
function addItem(cart, item) {
cart.items.push(item); // mutates the original array
return cart;
}
Si ese objeto cart está siendo observado en otro lugar, dentro del estado de React, en un almacén Redux o en cualquier sistema que detecte cambios comparando referencias, este tipo de mutación in situ es completamente invisible para él. La referencia en sí nunca cambia, por lo que no se dispara ninguna reconstrucción de la interfaz, ningún suscriptor recibe notificación, y uno termina intentando actualizar una UI que misteriosamente se niega a actualizarse.
function addItem(cart, item) {
return { ...cart, items: [...cart.items, item] };
}
Crear un objeto o array completamente nuevo en lugar de modificar el original sí consume un poco más de memoria. A cambio, se obtienen cambios en el estado predecibles, lo cual es un intercambio que vale la pena hacer con mucha más frecuencia de lo que parece.
3. No manejar las rechazos de Promise
Una función async que lanza un error sin estar envuelta en try/catch, o una cadena de .then() que carece de .catch(), tiende a fallar silenciosamente. En el navegador podría no producir ningún efecto visible; en Node puede generar una advertencia de rechazo no manejado que es fácil pasar por alto entre el resto de los registros.
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
getUser(42); // if this fails, where does the error go?
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return await res.json();
} catch (err) {
logger.error("Failed to fetch user", { id, err });
throw err;
}
}
Cualquier función async susceptible de fallar necesita una estrategia explícita para ese caso de fallo. Dejarla sin manejo no es realmente una estrategia, sino simplemente un error que esperará ocurrir más tarde.
4. Callbacks profundamente anidados
Nadie tiene la intención de escribir código con “infierno de callbacks” a propósito. Este problema se va acumulando gradualmente, con un paso asíncrono adicional cada vez, hasta que el código está anidado a seis niveles de profundidad y la sangría de espacios parece una escalera:
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getShipping(orders[0].id, (shipping) => {
updateUI(shipping); // and it keeps going
});
});
});
async/await se introdujo específicamente para evitar este tipo de anidamiento:
async function loadShippingInfo(id) {
const user = await getUser(id);
const orders = await getOrders(user.id);
const shipping = await getShipping(orders[0].id);
updateUI(shipping);
}
El comportamiento asíncrono subyacente es idéntico, pero ahora la lógica se lee de arriba hacia abajo, más o menos como uno la narraría en voz alta.
5. Variables globales que se infiltran
Si se omite una declaración de const, let o var fuera del modo estricto, JavaScript vinculará silenciosamente esa variable al objeto global en lugar de generar un error:
function calculateTotal() {
total = 0; // no declaration — this is now global
for (const item of items) total += item.price;
return total;
}
La variable total ahora se encuentra fuera del ámbito de la función, por lo que puede chocar con cualquier otra variable del mismo nombre en otras partes del código, ya sea sobrescribiendo algo else o siendo sobrescrita ella misma dependiendo del orden de ejecución. Al agregar "use strict" al principio de un archivo, esto se convierte en un error inmediato y visible en lugar de uno silencioso y diferido. La sintaxis moderna de módulos que utiliza import/export aplica automáticamente el modo estricto, por lo que este tipo de error se vuelve mucho más raro una vez que se trabaja con módulos ES.
6. Comparación de objetos y arrays con ===
Este error suele aparecer en personas que han tomado la regla n.º 1 un poco demasiado al pie de la letra. === compara objetos y arrays por referencia, no por su contenido:
{ a: 1 } === { a: 1 } // false
[1, 2, 3] === [1, 2, 3] // false
Dos objetos que parecen idénticos siguen siendo entidades separadas en la memoria, por lo que la igualdad estricta los trata como desiguales. Para comparar su contenido real, se necesita un enfoque de comparación profunda: una función auxiliar como isEqual de lodash, JSON.stringify para casos sencillos, o una rutina de comparación personalizada. Usar === aquí no es un error de sintaxis, sino que simplemente responde a una pregunta diferente a la que realmente se quería responder.
7. No eliminar los listeners de eventos y temporizadores
Cada llamada a addEventListener, setInterval o cualquier suscripción representa una promesa de que algo eventualmente la eliminará. Si se descuida esa promesa, se genera una fuga de memoria, algo fácil de pasar por alto durante el desarrollo pero muy costoso una vez que el sistema está en producción:
useEffect(() => {
window.addEventListener("resize", handleResize);
// no cleanup — this listener never goes away
}, []);
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
8. Uso excesivo de any en TypeScript
any no es realmente un tipo, sino más bien una vía de escape; recurrir a él con demasiada frecuencia convierte un código fuertemente tipado en uno no tipado, silenciosamente, sin que nadie haya elegido realmente ese resultado:
function processPayment(data: any) {
return data.amount * data.rate; // no safety net at all
}
Cada vez que modificas data aquí, estás haciendo conjeturas. El compilador no puede detectar errores de escritura, propiedades faltantes o tipos incompatibles, porque le has indicado explícitamente que deje de verificarlo. Incluso un tipo definido de forma laxa es mejor que no tener ningún tipo:
type PaymentData = { amount: number; rate: number };
function processPayment(data: PaymentData) {
return data.amount * data.rate;
}
Si realmente aún no conoces la estructura de algo, unknown es el equivalente honesto a any. Te obliga a definir con mayor precisión el tipo antes de poder usarlo, en lugar de permitirte actuar sobre una suposición no verificada.
9. Ignorar la diferencia entre null y undefined
JavaScript ofrece dos formas distintas de expresar “no hay nada aquí”, y los conjuntos de código que las utilizan de manera inconsistente terminan repletos de comprobaciones como esta:
if (value === null || value === undefined) { ... }
Ese patrón suele ser señal de que nadie llegó a acordar una convención. Un enfoque más ordenado consiste en asignarle a cada valor un significado específico: undefined significa “esto nunca se estableció”, y null significa “esto se estableció deliberadamente en nada”. A partir de ahí, el operador de unión nula permite verificar ambos al mismo tiempo, sin tener que escribir la comparación dos veces:
const displayName = user.nickname ?? "Anonymous";
?? solo entra en acción cuando el valor de la izquierda es null o undefined. Esto es diferente de ||, que también recurre a valores como 0, "" o false, valores que con frecuencia son válidos y no deberían considerarse faltantes.
10. Escribir código para la computadora en lugar de para otra persona
El último elemento de esta lista no es un error de sintaxis, pero causa más daño acumulativo que los otros nueve juntos. Una sola línea muy compleja puede resultar gratificante de escribir, pero costosa de leer para cualquier otra persona:
const r = a.filter(x=>x.a).map(x=>x.b).reduce((a,b)=>a+b,0);
Funciona. Pero quienquiera que lo lea después, incluido usted en unos meses, debe hacer ingeniería inversa para entender qué representan realmente a, x y el resto de la cadena antes de realizar cualquier cambio seguro.
const activeUserBalances = users
.filter((user) => user.isActive)
.map((user) => user.balance);
const totalActiveBalance = activeUserBalances.reduce((sum, balance) => sum + balance, 0);
Esta versión requiere un par de líneas adicionales, pero resulta completamente clara sin necesidad de ninguna decodificación. JavaScript tiende a recompensar la ingeniosidad en el momento pero a cobrar por ella más tarde, y ese “más tarde” casi siempre lo paga alguien distinto a quien escribió originalmente el código.
El patrón subyacente a los diez
Más allá de los detalles específicos, ninguno de estos diez puntos se refiere realmente a curiosidades poco conocidas sobre JavaScript. Todos tienen que ver con la previsibilidad: comparaciones que se comportan tal como se escriben, un estado que no cambia a espaldas del programador, errores que se detectan en lugar de desaparecer silenciosamente, y un código cuya estructura coincide con lo que realmente hace. Al abordar estos diez hábitos, lo que queda es la depuración habitual, esa que existe en cualquier base de código, en lugar de esa autogenerada que consume silenciosamente tu tiempo libre.
Lecturas relacionadas
- Errores comunes de JavaScript y TypeScript que roban el código en silencio — Explica problemas sutiles de JavaScript y TypeScript, desde comparaciones con NaN hasta cuestiones de temporización asíncrona y coerción de tipos, que causan errores a pesar de parecer correctos.
- Características de Node.js 26 que sustituyen a años de soluciones temporales — Un recorrido por la API Temporal de Node.js 26, la ejecución nativa de TypeScript, los ayudantes de caché y otras adiciones que eliminan soluciones temporales que han existido durante mucho tiempo.