Un stack trace es la lista de llamadas a función que estaban en curso cuando algo lanzó una excepción. El navegador lo escribe de dentro hacia fuera: la función que falló arriba del todo, la que la llamó justo debajo, y así sucesivamente hasta llegar a lo que inició todo el proceso.

Parece un muro de texto y se trata como tal. La mayoría de la gente lee la primera línea, busca el mensaje y nunca mira el resto, lo cual es una lástima, porque la primera línea dice qué se rompió y las líneas de debajo dicen por qué alguien estaba ahí.

Qué son realmente las líneas

Un trace de navegador tiene más o menos este aspecto:

TypeError: Cannot read properties of undefined (reading 'total')
    at formatPrice (checkout.js:214:19)
    at renderSummary (checkout.js:188:7)
    at onCartLoaded (cart.js:97:5)
    at HTMLButtonElement.<anonymous> (cart.js:41:12)

Ahí dentro hay cuatro cosas, y cada una responde a una pregunta distinta.

La primera línea es el error: un tipo y un mensaje. Dice qué salió mal en el momento en que salió mal, y es la línea menos útil para encontrar la causa. En el ejemplo, algo dentro de formatPrice intentó leer .total de un valor que no estaba ahí, que es el caso de la propiedad undefined y casi nunca es culpa de formatPrice.

Cada línea at es un frame: una función que estaba esperando a que la de encima devolviera el control. formatPrice fue llamada por renderSummary, que a su vez fue llamada por onCartLoaded.

Los números son archivo, línea y columna. checkout.js:214:19 es la línea 214, columna 19, lo cual importa cuando varias llamadas están en una misma línea.

El frame de más abajo es el disparador, y suele ser la línea individual más informativa de todo el trace. HTMLButtonElement.<anonymous> significa que un manejador de clic inició esto. Algo que hizo una persona, no algo que hizo la página al cargar. Eso ya es un paso de reproducción.

Léalo de abajo hacia arriba

El hábito que vale la pena adquirir es leer el trace en el orden en que el código realmente se ejecutó, es decir, de abajo hacia arriba.

Se disparó un clic. Se ejecutó onCartLoaded. Llamó a renderSummary, que llamó a formatPrice, que no encontró nada donde esperaba el total del carrito. Leído así, la pregunta deja de ser “qué le pasa a formatPrice” y pasa a ser “qué tenía onCartLoaded entre manos, y de dónde lo sacó”. Ahí es donde casi siempre está la solución.

Débil
Error en formatPrice, línea 214. Se añadió una comprobación de undefined.
Mejor
El manejador de clic en cart.js:41 se ejecutó antes de que llegara la respuesta del carrito, así que renderSummary recibió un objeto vacío. Se corrigió el orden; formatPrice no cambió.

La primera versión hace que desaparezca el error. La segunda hace que desaparezca el bug. No es el mismo trabajo, y el trace es lo que permite distinguirlos.

Los frames que no son suyos

La mayoría de los traces reales son más largos que el ejemplo y están llenos de frames de librerías: React, jQuery, el runtime de un bundler, un polyfill. La mayor parte del tiempo son ruido, y los dos navegadores principales pueden ocultarlos por usted.

En Chrome DevTools la función se llama Ignore list, dentro de Settings, y colapsa los frames de los scripts que usted indique, evitando que el depurador entre en ellos. Por defecto ya ignora /node_modules/ y /bower_components/, así que el primer paso útil suele ser añadirle sus propias rutas de librerías externas. El depurador de Firefox tiene la misma idea, por archivo, como Ignore source. De cualquier forma, cada trace posterior queda a la mitad de longitud.

Lo que queda es su propio código, que es la parte que puede cambiar.

Hay una excepción que conviene vigilar: si los únicos frames son de librerías, es probable que la llamada haya venido de dentro de esa librería y no de su código, y la causa suele estar en los datos o las opciones que le pasó.

Por qué el trace a veces apunta a algo sin sentido

Hay tres cosas que habitualmente hacen que un trace sea ilegible, y las tres tienen que ver con cómo se entregó el código, no con el bug en sí.

Minificación. En producción su código es una sola línea enorme con nombres de una letra, así que el trace muestra algo como at n (app.min.js:1:84213). El número de línea es real e inútil. La solución son los source maps: el build genera un archivo .map, el navegador lo aplica, y DevTools muestra los nombres de archivo y las líneas originales. Compruebe si los suyos se están generando, y si el navegador puede acceder a ellos, antes de concluir que un trace no tiene remedio.

Los límites asíncronos. Un trace termina donde termina la pila de llamadas, y un setTimeout, un callback de una promesa o un event listener inician una nueva. El frame que programó el trabajo no aparece en el trace por defecto. Async stack traces de Chrome está activado en DevTools y une ambas partes, por lo que un trace se ve más completo en DevTools que el mismo error en un archivo de log.

Scripts de otro origen. Si un script se sirve desde otro dominio sin permiso para exponer sus errores, el navegador se niega directamente a decir nada sobre él: el mensaje se convierte en la cadena escueta Script error., sin archivo, sin línea y sin trace. La solución está en el lado del servidor: Access-Control-Allow-Origin en el script, más crossorigin="anonymous" en la etiqueta. Vale la pena saberlo, porque el síntoma parece un error misterioso y en realidad es una regla de permisos.

El trace que no llegó a recoger

Todo lo anterior parte de que usted tiene el trace. El caso incómodo es el que más importa: el error ocurrió en la máquina de otra persona, en un navegador que usted no tiene, y lo único que le llegó es una frase.

Un stack trace no es algo que se le pueda pedir a un usuario. Vive en un panel que esa persona nunca abrió, desaparece en cuanto cierra la pestaña, y la consola no sobrevive a una navegación de página a menos que alguien haya pensado en marcar Preserve log de antemano. Así que la elección práctica está entre contar con un error tracker o una herramienta de reporte que capture la consola en el momento del fallo, o prescindir de ello.

Sin eso, el reporte dice “la página de checkout se quedó en blanco”, y las cuatro líneas que lo habrían resuelto en un minuto fueron descartadas por el navegador segundos después.

Session Replay

Extensión gratuita para Chrome. Un clic en la página que está fallando toma la captura de pantalla, la consola y el registro de red, y le entrega un enlace para pegar en el ticket.

Obtener la extensión

Conseguir uno a propósito

Hay dos cosas que vale la pena conocer para cuando quiera un trace en lugar de esperar a que aparezca uno.

console.trace() imprime la pila de llamadas actual sin lanzar nada, lo cual es la forma más rápida de responder “quién está llamando a esta función” en un código base que usted no conoce.

new Error().stack le da lo mismo como una cadena de texto, que es lo que envían los error trackers. También es la forma de adjuntar un trace a un error capturado que de otro modo desaparecería: capturar una excepción y registrar solo e.message descarta toda la pila, y esa costumbre es responsable de muchos logs de producción ilegibles.

Qué sacar de todo esto

La primera línea nombra el síntoma. La última línea nombra lo que lo inició. El medio es el camino entre ambas, y la solución suele estar en algún punto de ese camino, no en ninguno de los dos extremos.

Léalo de abajo hacia arriba, oculte los frames que no son suyos y revise sus source maps antes de culpar al trace. Y si el trace que necesita pertenece a la sesión del navegador de otra persona, el momento de organizarlo es antes del bug, no después: para cuando llega el reporte, ese trace ya ha desaparecido.