Back

Trampas de JavaScript: Cinco Problemas que Verás una y Otra Vez

Trampas de JavaScript: Cinco Problemas que Verás una y Otra Vez

Has enviado código que pasó el linting, funcionó en desarrollo y aun así falló en producción. El bug parecía obvio en retrospectiva: un await faltante, un array mutado, un this que apuntaba a un lugar inesperado. Estas trampas de JavaScript persisten porque la flexibilidad del lenguaje crea sutiles trampas que las herramientas modernas no siempre detectan.

Aquí hay cinco errores comunes de JS que continúan apareciendo en bases de código del mundo real, junto con formas prácticas de evitarlos.

Puntos Clave

  • Usa igualdad estricta (===) para evitar comportamientos inesperados de coerción de tipos
  • Las funciones flecha preservan this de su ámbito envolvente, mientras que las funciones regulares vinculan this dinámicamente
  • Prefiere const y declara variables al inicio de su ámbito para evitar errores de zona muerta temporal
  • Usa Promise.all para operaciones asíncronas en paralelo y Promise.allSettled cuando necesites resultados parciales
  • Usa métodos de array no mutantes como toSorted() y structuredClone() para copias profundas

La Coerción de Tipos Aún Sorprende

El operador de igualdad flexible de JavaScript (==) realiza coerción de tipos, produciendo resultados que parecen ilógicos hasta que comprendes el algoritmo subyacente.

0 == '0'           // true
0 == ''            // true
'' == '0'          // false
null == undefined  // true
[] == false        // true

La solución es directa: usa igualdad estricta (===) en todas partes. Pero la coerción aparece en otros contextos también. El operador + concatena cuando cualquier operando es una cadena:

const quantity = '5'
const total = quantity + 3 // '53', no 8

Las mejores prácticas modernas de JavaScript sugieren conversión explícita con Number(), String() o template literals cuando la intención importa. La coalescencia nula (??) también ayuda aquí: solo recurre al valor alternativo con null o undefined, a diferencia de || que trata 0 y '' como falsy.

El Problema de Vinculación de this

El valor de this depende de cómo se llama una función, no de dónde está definida. Esta sigue siendo una de las trampas de JavaScript más persistentes.

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name)
  }
}

const greet = user.greet
greet() // undefined—'this' ahora es el objeto global

Las funciones flecha capturan this de su ámbito envolvente, lo que resuelve algunos problemas pero crea otros cuando realmente necesitas vinculación dinámica:

const user = {
  name: 'Alice',
  greet: () => {
    console.log(this.name) // 'this' se refiere al ámbito externo, no a 'user'
  }
}

Usa funciones flecha para callbacks donde quieras preservar el contexto. Usa funciones regulares para métodos de objeto. Al pasar métodos como callbacks, vincula explícitamente o envuelve en una función flecha.

Hoisting y la Zona Muerta Temporal

Las variables declaradas con let y const se elevan (hoisting) pero no se inicializan, creando una zona muerta temporal (TDZ por sus siglas en inglés) donde acceder a ellas lanza un ReferenceError:

console.log(x) // ReferenceError
let x = 5

Esto difiere de var, que se eleva e inicializa a undefined. La TDZ existe desde el inicio del bloque hasta que se evalúa la declaración.

Las declaraciones de función se elevan completamente, pero las expresiones de función no:

foo() // funciona
bar() // TypeError: bar is not a function

function foo() {}
const bar = function() {}

Declara variables al inicio de su ámbito y prefiere const por defecto. Esto elimina sorpresas de TDZ y señala la intención claramente.

Trampas Asíncronas en JavaScript

Olvidar await es común, pero errores async más sutiles causan más daño. Awaits secuenciales cuando es posible la ejecución paralela desperdician tiempo:

// Lento: se ejecuta secuencialmente
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()

// Rápido: se ejecuta en paralelo
const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments()
])

Otro problema frecuente: Promise.all falla rápido. Si una promesa se rechaza, pierdes todos los resultados. Usa Promise.allSettled cuando necesites resultados parciales:

const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
const successful = results.filter(r => r.status === 'fulfilled')

Siempre maneja los rechazos. Los rechazos de promesas no manejados pueden bloquear procesos de Node y causar fallos silenciosos en navegadores.

Mutación vs Inmutabilidad en JavaScript

Mutar arrays y objetos crea bugs difíciles de rastrear, especialmente en frameworks con estado reactivo:

const original = [3, 1, 2]
const sorted = original.sort() // ¡Muta el original!
console.log(original) // [1, 2, 3]

JavaScript moderno proporciona alternativas no mutantes. Usa toSorted(), toReversed() y with() para arrays. Para objetos, la sintaxis spread crea copias superficiales:

const sorted = original.toSorted()
const updated = { ...user, name: 'Bob' }

Recuerda que spread crea copias superficiales. Los objetos anidados aún comparten referencias:

const copy = { ...original }
copy.nested.value = 'changed' // También cambia original.nested.value

Para clonación profunda, usa structuredClone() o maneja estructuras anidadas explícitamente.

Conclusión

Estos cinco problemas—coerción de tipos, vinculación de this, hoisting, mal uso de async y mutación accidental—representan una proporción desmedida de bugs de JavaScript. Reconocerlos en revisiones de código se vuelve automático con la práctica.

Habilita reglas estrictas de ESLint como eqeqeq y no-floating-promises. Considera TypeScript para proyectos donde la seguridad de tipos importa. Lo más importante, escribe código que haga la intención explícita en lugar de depender de los comportamientos implícitos de JavaScript.

Preguntas Frecuentes

JavaScript incluye ambos por razones históricas y flexibilidad. El operador de igualdad flexible (==) realiza coerción de tipos antes de la comparación, lo que puede ser útil pero a menudo produce resultados inesperados. El operador de igualdad estricta (===) compara tanto el valor como el tipo sin coerción. La mejor práctica moderna favorece fuertemente === porque hace las comparaciones predecibles y reduce bugs.

Usa funciones flecha para callbacks, métodos de array y situaciones donde quieras preservar el contexto this circundante. Usa funciones regulares para métodos de objeto, constructores y casos donde necesites vinculación dinámica de this. La diferencia clave es que las funciones flecha vinculan léxicamente this desde su ámbito envolvente, mientras que las funciones regulares determinan this según cómo se llaman.

La zona muerta temporal (TDZ) es el período entre entrar en un ámbito y el punto donde se declara una variable let o const. Acceder a la variable durante este período lanza un ReferenceError. Evita problemas de TDZ declarando variables al inicio de su ámbito y prefiriendo const por defecto. Esto hace tu código más predecible y fácil de leer.

Usa Promise.all cuando todas las promesas deben tener éxito para que tu operación sea significativa—falla rápido si alguna promesa se rechaza. Usa Promise.allSettled cuando necesites resultados de todas las promesas independientemente de fallos individuales, como al obtener datos de múltiples fuentes opcionales. Promise.allSettled devuelve un array de objetos que describen cada resultado como fulfilled o rejected.

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.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay