Back

Solución del error 'Maximum call stack size exceeded' en JavaScript

Solución del error 'Maximum call stack size exceeded' en JavaScript

Estás mirando tu consola y ahí está: RangeError: Maximum call stack size exceeded. En Firefox, podría decir too much recursion. De cualquier manera, tu aplicación acaba de fallar y necesitas entender por qué, y cómo solucionarlo correctamente.

Este error indica un desbordamiento de pila (stack overflow): tu código ha realizado tantas llamadas de función anidadas que el motor de JavaScript se quedó sin espacio para rastrearlas. Exploremos qué desencadena esto en JavaScript moderno, cómo depurarlo eficazmente y cómo prevenirlo en aplicaciones React, Next.js y Node.js.

Puntos Clave

  • La pila de llamadas tiene un tamaño finito que varía según el entorno, así que nunca escribas código que dependa de alcanzar un límite específico.
  • Las causas comunes incluyen recursión infinita, recursión mutua, re-renderizados infinitos en React y referencias circulares en la serialización JSON.
  • Depura habilitando “Pause on Exceptions” en DevTools y buscando patrones de funciones repetidas en la pila de llamadas.
  • Soluciona estructuralmente convirtiendo la recursión en iteración, usando trampolining o manejando referencias circulares con una función replacer.

Qué Hace Realmente la Pila de Llamadas

Cada vez que se ejecuta una función, el motor de JavaScript crea un marco de pila (stack frame) que contiene las variables locales de la función, los argumentos y la dirección de retorno. Estos marcos se apilan uno encima del otro. Cuando una función retorna, su marco se elimina.

¿El problema? Esta pila tiene un tamaño finito. El límite exacto varía según el entorno: Chrome podría permitir alrededor de 10,000-15,000 marcos, mientras que Firefox permite aproximadamente 50,000. Node.js típicamente limita alrededor de 11,000 marcos por defecto.

Importante: Estos números dependen de la implementación y pueden cambiar entre versiones. No escribas código que dependa de alcanzar un límite específico.

Patrones Comunes que Provocan Desbordamiento de Pila

Recursión Infinita Clásica

El caso de libro de texto: una función que se llama a sí misma sin una condición de salida adecuada.

function processItem(item) {
  // Falta caso base
  return processItem(item.child)
}

Recursión Mutua

Dos funciones llamándose entre sí en un bucle:

function isEven(n) {
  return n === 0 ? true : isOdd(n - 1)
}

function isOdd(n) {
  return n === 0 ? false : isEven(n - 1)
}

isEven(100000) // Desbordamiento de pila

Re-renderizados Infinitos en React

Aquí es donde muchos desarrolladores frontend encuentran el error. Las actualizaciones de estado durante el renderizado crean bucles infinitos:

function BrokenComponent() {
  const [count, setCount] = useState(0)
  setCount(count + 1) // Dispara re-renderizado inmediatamente
  return <div>{count}</div>
}

Las dependencias incorrectas en useEffect causan problemas similares:

useEffect(() => {
  setData(transformData(data)) // data cambia, el efecto se ejecuta de nuevo
}, [data])

Desbordamiento de Pila por Referencia Circular en JSON

Cuando los objetos se referencian a sí mismos, JSON.stringify recurre infinitamente:

const obj = { name: 'test' }
obj.self = obj
JSON.stringify(obj) // Maximum call stack size exceeded

Depuración del Desbordamiento de Pila en Node.js y Navegadores

Paso 1: Habilitar “Pause on Exceptions”

En Chrome DevTools, abre el panel Sources y habilita “Pause on caught exceptions”. Para Node.js, usa la bandera --inspect y conecta Chrome DevTools.

Paso 2: Inspeccionar la Pila de Llamadas en Busca de Marcos Repetidos

Cuando el depurador se pause, examina el panel de pila de llamadas. Busca patrones repetidos: la misma función apareciendo docenas o cientos de veces indica tu punto de recursión.

Paso 3: Usar Trazas de Pila Asíncronas

Los DevTools modernos muestran trazas de pila asíncronas por defecto. Esto ayuda cuando la recursión abarca cadenas de Promise o callbacks de setTimeout.

console.trace() // Imprime la traza de pila actual

Nota: Aumentar el tamaño de la pila con node --stack-size es una herramienta de diagnóstico, no una solución. Retrasa el fallo pero no corrige el error.

Soluciones Prácticas que Realmente Funcionan

Convertir Recursión en Iteración

La mayoría de los algoritmos recursivos pueden convertirse en iterativos con una pila explícita:

function processTree(root) {
  const stack = [root]
  while (stack.length > 0) {
    const node = stack.pop()
    process(node)
    if (node.children) {
      stack.push(...node.children)
    }
  }
}

Usar Trampolining para Recursión Profunda

Los trampolines dividen la recursión en pasos, evitando el crecimiento de la pila:

function trampoline(fn) {
  return function(...args) {
    let result = fn(...args)
    while (typeof result === 'function') {
      result = result()
    }
    return result
  }
}

Manejar Referencias Circulares de Forma Segura

Para la serialización JSON, usa una función replacer o bibliotecas como flatted:

const seen = new WeakSet()
JSON.stringify(obj, (key, value) => {
  if (typeof value === 'object' && value !== null) {
    if (seen.has(value)) {
      return '[Circular]'
    }
    seen.add(value)
  }
  return value
})

Prevenir Recursión Infinita en React

Asegúrate siempre de que las dependencias de useEffect sean estables y las actualizaciones de estado sean condicionales:

useEffect(() => {
  if (!isProcessed) {
    setData(transformData(rawData))
    setIsProcessed(true)
  }
}, [rawData, isProcessed])

Por Qué la Optimización de Llamadas en Cola No Te Salvará

ES6 especificó llamadas en cola apropiadas (proper tail calls), que teóricamente permitirían recursión infinita en posición de cola. En la práctica, solo Safari implementa esto. Chrome y Firefox no lo hacen, y Node.js lo deshabilitó. No confíes en TCO: refactoriza tu código en su lugar.

Conclusión

El error “Maximum call stack size exceeded” es un bug de alta severidad que requiere correcciones estructurales. No puedes solucionarlo con try-catch, y no deberías intentar aumentar los límites de pila en producción.

Encuentra el patrón repetido en tu traza de pila, luego agrega condiciones de terminación apropiadas, convierte a iteración o divide el trabajo en fragmentos asíncronos. Trata las referencias circulares como problemas de estructura de datos, no como problemas de serialización.

Cuando veas este error, tu código te está diciendo que algo fundamental necesita cambiar.

Preguntas Frecuentes

Técnicamente sí, pero no es una solución confiable. Para cuando este error se lanza, tu aplicación está en un estado inestable. La pila está agotada, y capturar el error no la restaura. Corrige el problema de recursión subyacente en lugar de intentar manejar la excepción.

Abre las DevTools de tu navegador, ve al panel Sources y habilita Pause on Exceptions. Cuando ocurra el error, examina el panel de pila de llamadas en busca de nombres de función repetidos. La función que aparece repetidamente es tu culpable. También puedes usar console.trace() para imprimir la pila en puntos específicos.

Llamar a setState directamente en el cuerpo del renderizado dispara un re-renderizado inmediato, que llama a setState de nuevo, creando un bucle infinito. Mueve las actualizaciones de estado a hooks useEffect con dependencias apropiadas, o a manejadores de eventos. Nunca actualices el estado incondicionalmente durante el renderizado.

En Node.js, puedes usar la bandera --stack-size para aumentar el límite, pero esto solo retrasa el fallo. Los navegadores no permiten cambios en el tamaño de la pila. Ningún enfoque corrige la causa raíz. Refactoriza tu código para usar iteración o patrones asíncronos en lugar de depender de recursión profunda.

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