Cómo leer un stack trace de JavaScript
Lee stack traces de JavaScript: identifica el primer frame útil, entiende los huecos async, el código minificado, los source maps y Error.cause.
Un stack trace de JavaScript se lee hacia atrás en el tiempo: el frame superior es la llamada que lanzó el error, y cada frame por debajo es la llamada que condujo hasta allí.
Lo habitual es echar un vistazo a la primera línea, pegarla en un buscador y cruzar los dedos. Eso funciona hasta que el frame superior pertenece a React, o a JSON.parse, o a un bundle donde todas las funciones se llaman o.
Este artículo recorre un mismo trace de principio a fin, añadiendo en cada sección una forma de leerlo: qué frame abrir realmente, qué te están diciendo los frames async, cómo se ve un trace minificado, y por qué el stack que está detrás de Error.cause nunca aparece en el stack que imprimiste.
Puntos clave
- El frame superior es donde se lanzó el error, no donde se introdujo el bug; el primer frame que apunta a un archivo que tú escribiste es donde empieza la investigación.
- Los frames async marcados con
asynclos reconstruye V8 a partir de los puntos donde cadaawaitse pausó y se reanudó, de modo queawait,Promise.all()yPromise.any()quedan enlazados, mientras que una cadena de.then()sin más deja un hueco. - V8 conserva solo 10 frames por defecto, un número que puedes modificar mediante la propiedad no estándar
Error.stackTraceLimit. new Error(message, { cause })preserva el error original, pero el motor no fusiona los dos stacks:err.stackmuestra únicamente el wrapper.
¿Cómo se ve un stack trace de JavaScript?
Un stack trace de JavaScript consiste en el nombre y el mensaje del error en la primera línea, seguidos de un frame por cada llamada, del más reciente al más antiguo. Este es el trace al que este artículo vuelve una y otra vez: un archivo de carrito se lee del disco, se parsea y se entrega al resto de la aplicación.
SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
at JSON.parse (<anonymous>)
at parseCart (/app/src/cart.js:5:15)
at loadCart (/app/src/cart.js:10:20)
at main (/app/src/main.js:6:16)
at Object.<anonymous> (/app/src/main.js:12:1)
Lee los frames en ese orden y la cadena queda clara: JSON.parse lanzó el error, parseCart lo llamó, loadCart llamó a parseCart, y así hacia abajo. El frame inferior es donde comenzó esa cadena de llamadas concreta, que no siempre es el punto de entrada del programa: el código alcanzado a través de un manejador de clic, un callback de temporizador o una promesa resuelta obtiene una cadena nueva que empieza en el callback.
Un solo frame te da cuatro cosas: el nombre de la función, el archivo, la línea y la columna. En código minificado, solo la columna vale algo sin un source map. Una advertencia que conviene conocer pronto: Error.prototype.stack no está estandarizado. Todos los motores lo incluyen, cada uno imprime una cadena ligeramente distinta, y el trabajo en TC39 para fijar el formato sigue inacabado. Los ejemplos aquí tienen forma de V8, lo que cubre Chrome, Edge y Node.
El frame superior normalmente no es tu código
El frame superior de un stack trace normalmente no es tu código. Es la librería, el framework o la función integrada que detectó el valor incorrecto, lo que significa que te dice qué se rompió, no por qué. En el trace anterior, at JSON.parse (<anonymous>) es el parser integrado informando de que una cadena que se le pasó no es JSON válido. Ahí no hay nada que arreglar.
El frame que te interesa es el primero que apunta a un archivo que tú escribiste. Ese es parseCart en /app/src/cart.js:5:15. Abre esa línea y encontrarás la llamada a JSON.parse, lo que te indica que la cadena incorrecta llegó como argumento. Así que el valor vino del frame de abajo: loadCart, en la línea 10, que leyó el archivo. Ese es el verdadero punto de partida, y la pregunta que responde es de dónde salió el contenido del archivo y por qué nada lo validó.
Ese orden de lectura se generaliza. Recorre hacia abajo, pasando por rutas de node_modules, frames <anonymous> y native, hasta que llegues a tu propio archivo; luego avanza hacia abajo por los frames que suministraron el valor.
Un trace registra el camino que tomó el programa hacia el fallo, nunca el camino que tomó el usuario, y por eso dos reportes con frames idénticos pueden ser una corrección de cinco minutos o un fantasma irreproducible. El session replay cierra esa mitad de la brecha: lees los frames para saber dónde se rompió y observas la sesión para entender cómo la aplicación llegó a un estado en el que eso podía romperse.
Frames async y el límite del await
Marca loadCart como async y el trace abarcará el await, con los frames reconstruidos precedidos por async:
SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
at JSON.parse (<anonymous>)
at parseCart (/app/src/cart.js:5:15)
at async loadCart (/app/src/cart.js:10:20)
at async main (/app/src/main.js:6:16)
Esos frames con el prefijo async no se capturan igual que los frames sincrónicos. V8 los reconstruye a partir de los puntos de await, y puede hacerlo sin coste porque un await se reanuda exactamente en el mismo punto donde se pausó.
La reconstrucción tiene límites, y esos límites son donde el trace se adelgaza. El enlazado llega a los puntos de await, Promise.all() y Promise.any(), y a nada más, así que una promesa devuelta sin ser esperada con await, o una cadena de .then(), deja un agujero justo donde estaba el contexto de llamada. Si los frames se detienen abruptamente en un límite async, busca un await ausente en la capa superior. No hay ningún flag que activar: --async-stack-traces está activado por defecto desde V8 v7.3, de modo que los frames async aparecen en Chrome, en todas las versiones de Node con soporte, y en otros runtimes actuales basados en V8.
La otra razón por la que faltan frames es el límite. V8 conserva 10 frames y descarta el resto, y Error.stackTraceLimit es el control de ese número: un nuevo valor se aplica a los errores creados después de asignarlo, y cualquier cosa que no sea un número, o que sea menor que cero, te deja sin ningún frame.
if (process.env.NODE_ENV !== 'production') {
Error.stackTraceLimit = Infinity;
}
Restríngelo a desarrollo. Los traces profundos consumen memoria, entierran los frames interesantes en ruido y empujan rutas de archivos y nombres de funciones hacia logs que pueden acabar enviándose a otros sitios. La propiedad no es estándar: salió de V8, y JavaScriptCore la copió por compatibilidad, así que asignarla no rompe nada en ningún sitio, pero el valor por defecto y los detalles finos dependen del motor.
¿Cómo se lee un trace minificado?
Contra un bundle de producción, el mismo fallo produce frames como estos. Toma la forma como algo ilustrativo de la salida de un bundler, más que como el formato exacto de una herramienta concreta:
SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
at JSON.parse (<anonymous>)
at o (/assets/index-4f1c8a2b.js:1:20874)
at async s (/assets/index-4f1c8a2b.js:1:21036)
La firma es inconfundible: nombres de función de una sola letra, un único nombre de archivo, línea 1 y una columna de cinco o seis dígitos. Línea 1 y una columna enorme significan que todo el grafo de módulos está en una sola línea, así que la columna es la única coordenada que aporta información. parseCart y loadCart siguen existiendo en ese desplazamiento de columna, pero nada en la cadena te dirá sus nombres.
Recuperarlos requiere un source map al que el tooling pueda acceder, generado en tiempo de build y subido a algún lugar contra el cual se pueda resolver el trace. Nuestra guía sobre cómo funcionan los source maps cubre el formato y la configuración del build.
Error.cause no fusiona stacks
Envolver un error con new Error(message, { cause: originalError }) preserva el objeto de error original junto con su tipo y su propio stack, pero el motor no fusiona los dos stacks. err.stack muestra solo el wrapper, y el original es accesible únicamente a través de err.cause.stack.
export async function loadCart(path) {
const raw = await readFile(path, 'utf8');
try {
return parseCart(raw);
} catch (err) {
throw new Error(`Cart file ${path} is not valid JSON`, { cause: err });
}
}
Quienes llaman ahora reciben un mensaje que nombra el archivo, y err.cause instanceof SyntaxError sigue siendo cierto. Lo que no reciben es el frame de JSON.parse: el stack del wrapper empieza en el throw dentro de loadCart. Error.cause llegó en ES2022 y funciona en los navegadores actuales y en todas las versiones de Node con soporte, hasta Node 16.9.0. Se define como una propiedad propia no enumerable, así que queda fuera de Object.keys(), de for...in y de un JSON.stringify() ingenuo del error.
Cuánta parte de una cadena imprime una consola varía según el runtime y según la consola, así que lo portable es recorrerla tú mismo:
function printChain(error) {
let current = error;
while (current instanceof Error) {
console.error(current.stack);
current = current.cause;
}
}
Eso imprime los frames del wrapper, y luego los del parser, en el orden en que fueron lanzados.
Dos hábitos que destruyen el rastro
Capturar un error y lanzar uno nuevo sin pasar una causa elimina el trace original del programa. Los frames que te habrían dicho por dónde entró el valor incorrecto ya no existen en ninguna parte, y ninguna cantidad de búsquedas en logs los traerá de vuelta:
catch (err) {
throw new Error('Could not load cart'); // JSON.parse frame is gone
}
Tragarse el error en una línea de log hace el mismo daño, pero más sigilosamente:
catch (err) {
console.log('cart load failed'); // message, no stack, no type
return [];
}
En ambos casos falta una sola palabra clave para que estén bien. Pasa { cause: err } cuando relances, y registra err en sí mismo en lugar de una frase sobre él.
La próxima vez que un trace aterrice delante de ti, no empieces por la línea uno. Baja hasta el primer frame que lleve el nombre de uno de tus archivos, abre esa línea y pregúntate qué valor se le entregó y quién lo hizo. Si los frames se detienen en un límite async o en una función de una sola letra, estás viendo un hueco en el enlazado o un source map que falta, no la historia completa.
Preguntas frecuentes
¿Por qué mi manejador de errores solo reporta 'Script error.' sin stack?
Los navegadores ocultan los detalles de las excepciones lanzadas por scripts de otro origen, así que window.onerror recibe el texto genérico 'Script error.' sin URL, número de línea ni stack útiles. Para obtener el mensaje y los frames reales, carga el script con el atributo crossorigin puesto en anonymous y asegúrate de que el servidor que lo aloja devuelva una cabecera Access-Control-Allow-Origin que cubra tu origen. La mayoría de las CDN públicas ya envían esa cabecera.
¿Funciona Error.captureStackTrace fuera de Chrome y Node?
Ya no es exclusivo de V8. Error.captureStackTrace nació en V8 como parte de su API no estándar de stack traces, y los demás motores lo han ido adoptando: JavaScriptCore lo incorporó en Safari 17.2, publicado el 11 de diciembre de 2023, y SpiderMonkey en Firefox 138, publicado el 29 de abril de 2025. Al invocarlo, escribe una cadena de stack en el objeto que le pases. Como sigue sin estar estandarizado, protege la llamada con una comprobación typeof Error.captureStackTrace antes de usarla en código de librerías compartidas.
¿Por qué los stack traces se ven distintos en Firefox que en Chrome?
Error.prototype.stack queda fuera de cualquier especificación, así que cada motor lo imprime como quiere y el contenido varía. V8 escribe cada frame en una línea que empieza por 'at', mientras que Firefox usa un formato functionName@file:line:column sin ese prefijo. Trata la cadena del stack como salida legible para humanos, no como una API parseable, y nunca construyas el agrupamiento de errores basándote solo en una expresión regular hecha a mano.
¿Puedo capturar un stack trace sin lanzar un error?
Sí. En la mayoría de los motores actuales el stack se rellena cuando construyes el Error, no cuando lo lanzas, así que const { stack } = new Error() te entrega la pila de llamadas al instante, sin throw ni catch. El frame superior es la línea que creó el error, y el límite habitual de frames sigue aplicándose: V8 conserva solo 10 frames a menos que se aumente Error.stackTraceLimit.
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.
Star on GitHub12k