Inicio / Artículos / Diez hábitos recurrentes de JavaScript que socavan silenciosamente tu base de código

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.

1748 palabras

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

  • Nueve hábitos a nivel de código que hacen más fácil confiar en el trabajo de los ingenieros seniores — Explora nueve prácticas concretas de programación, desde cláusulas de protección hasta un modelado estricto de datos, que hacen que el código sea más resistente, legible y fácil de depurar bajo presión.
  • Diez hábitos de arquitectura que mantienen las base de código frontend mantenibles durante años — Explica hábitos estructurales como la optimización para la eliminación, el flujo de datos explícito y el aislamiento de la lógica empresarial, que ayudan a que las bases de código sigan siendo mantenibles a lo largo de los años de cambios.
  • Arqueología de software: un método práctico para leer código legado — Aprenda un enfoque paso a paso para investigar de forma segura bases de código legado no documentadas, desde analizar el historial de commits hasta refactorizar sin interrumpir la operación del sistema.
  • Diez reglas cotidianas de JavaScript que destrozan su modelo mental — Explora diez comportamientos sutiles de JavaScript, desde la mutabilidad de const hasta los cierres y el manejo asincrónico de errores, que causan errores silenciosamente en el código de desarrolladores experimentados.
  • Siete idiomas comunes de JavaScript que introducen en silencio problemas futuros — Explica cómo los patrones cotidianos de JavaScript como las comprobaciones de valor verdadero, la cadena opcional y la sintaxis de propagación ocultan suposiciones que se rompen en silencio a medida que el código evoluciona.