wandres.dev
SOURCES I · Depurar con breakpoints

Leer el call stack y navegar por los marcos

Qué es cada línea de la pila, cómo cambia el estado al seleccionar un marco, y la técnica de reiniciar un marco para volver a ejecutar sin recargar.

⏱ 15 min

La pila de llamadas es la respuesta a la pregunta de cómo has llegado hasta aquí, y en depuración esa pregunta suele valer más que la de qué hay en las variables. Un valor incorrecto rara vez se produce donde lo descubres: se produce arriba, en el marco que lo calculó, y la pila es el camino que lleva hasta él. Además, seleccionar un marco distinto no es solo mirar: cambia el contexto de todo el depurador, incluido el de la consola.

🎯 Al terminar esta lección sabrás
  • Interpretar cada línea de la pila y reconstruir el recorrido de la ejecución.
  • Cambiar de marco y entender qué cambia en el scope y en la consola al hacerlo.
  • Reiniciar un marco para volver a ejecutar una función sin recargar la página.
  • Aplicar la bisección sobre la pila para localizar dónde se corrompió un valor.

Qué es cada línea

El subpanel de pila de llamadas muestra, de arriba abajo, la función en la que estás detenido, la que la llamó, la que llamó a esa, y así hasta la raíz. Cada línea muestra el nombre de la función y el fichero con la línea desde donde se hizo la llamada.

Dos detalles de lectura.

El nombre puede ser el de la función declarada, el inferido de la asignación, o algo genérico si es anónima. Con source maps aplicados, es el nombre original; sin ellos, en código minificado, es una letra.

La posición de cada marco es la línea donde está la llamada, no donde empieza la función. Eso importa: el marco que dice procesarPedido en la línea 120 significa que en la línea 120 de esa función hay una llamada que llevó al marco de arriba.

En la raíz de la pila suele haber un marco que no es tuyo: un manejador de eventos, un callback de temporizador, la resolución de una promesa, o el punto de entrada del módulo. Ese marco te dice qué originó todo el flujo, y es de los datos más informativos de la pila.

ℹ️
Nota

Con la lista de scripts ignorados bien configurada, los marcos de librerías se agrupan y se pliegan, dejando visible solo tu código. En una aplicación con framework moderno esa diferencia es de treinta marcos a cuatro, y es lo que hace la pila legible.

Cambiar de marco lo cambia todo

Pulsar en un marco de la pila no es solo mirar. Al seleccionarlo, cuatro cosas cambian a la vez.

El editor salta al fichero y a la línea de ese marco.

El panel de scope muestra las variables de ese marco: sus parámetros, sus locales, sus closures.

Las expresiones vigiladas se reevalúan en ese contexto.

Y la consola evalúa en ese ámbito. Escribir el nombre de una variable local del marco seleccionado funciona, aunque esa variable no exista en el marco donde estás detenido.

Esa última es la más potente y la que menos gente aprovecha. Permite inspeccionar el estado de cualquier punto de la cadena de llamadas sin haber puesto un breakpoint ahí. Estás detenido dentro de una función de utilidad y quieres saber con qué objeto entró el componente que la llamó tres marcos más arriba: seleccionas ese marco y lo consultas.

La bisección sobre la pila

La técnica que convierte la pila en una herramienta de diagnóstico y no en información de contexto.

Cuando un valor es incorrecto donde lo has descubierto, la pregunta es en qué punto de la cadena dejó de ser correcto. Con n marcos, la respuesta se encuentra en log(n) comprobaciones.

Selecciona el marco del medio. Consulta el valor en ese contexto. Si ya estaba mal, el problema está por encima; si estaba bien, está por debajo. Repite con la mitad correspondiente.

Con una pila de dieciséis marcos, cuatro comprobaciones localizan la función exacta donde se corrompió. La alternativa —poner breakpoints en cada nivel y recargar— son dieciséis reproducciones.

Reiniciar un marco

El menú contextual de un marco ofrece reiniciarlo. Al hacerlo, la ejecución vuelve al principio de esa función, con los mismos argumentos, y puedes recorrerla otra vez.

Es la respuesta al “me he pasado de largo” que ocurre continuamente: has pulsado pasar por encima una vez de más y la llamada que querías inspeccionar ya se ha ejecutado. Reiniciando el marco vuelves a antes de ella.

Tiene un límite importantísimo que hay que entender para no producir bugs fantasma: reinicia la ejecución de la función, no deshace sus efectos. Si la primera pasada modificó un objeto, escribió en el almacenamiento, envió una petición o mutó el DOM, todo eso sigue hecho. Reiniciar una función que añade un elemento a un array lo añade dos veces.

Por eso funciona bien con funciones puras o casi puras, y produce estados imposibles con funciones que mutan. Y por eso, cuando reinicias un marco y el comportamiento cambia respecto a la primera vez, la primera sospecha debe ser que estás observando el efecto de tu propio reinicio.

Además, no todos los marcos se pueden reiniciar: los marcos asíncronos reconstruidos y los que están en la raíz de la pila no lo admiten.

Reconstruir el flujo desde la pila

El ejercicio que convierte la pila en comprensión. Detenido en cualquier punto de una aplicación desconocida, leer la pila de abajo arriba cuenta la historia completa.

El marco inferior dice qué originó el flujo: un clic, un temporizador, una respuesta de red, el arranque del módulo.

Los marcos intermedios dicen por qué capas ha pasado: el enrutador, el gestor de estado, el componente, la utilidad.

El marco superior dice dónde está el detalle que estás mirando.

Esa lectura, hecha una vez sobre una aplicación que no conoces, enseña más de su arquitectura que media hora leyendo ficheros, porque muestra las capas reales y no las que dice el diagrama.

El marco que falta es tan informativo como los que están

Hay una forma de leer la pila que solo se le ocurre a quien lleva tiempo depurando y que resuelve casos que parecen imposibles: fijarse en lo que no está. Cuando te detienes en un breakpoint y la pila no contiene el marco que esperabas —la función que según tu modelo mental tenía que haber llamado a esta— tienes una información valiosísima, porque significa que existe un camino de ejecución que no conocías. Los casos concretos son cuatro y cada uno tiene una firma reconocible. Si la pila es anormalmente corta y arranca directamente en un manejador de eventos o en un callback de temporizador, la llamada no vino de donde creías sino de una entrada asíncrona directa, y la explicación suele ser un listener duplicado o un temporizador que nadie limpió. Si la pila contiene el mismo par de funciones repetido muchas veces, tienes una recursión, intencionada o no. Si la pila pasa por un marco de framework que no esperabas —un planificador, una cola de efectos, un reconciliador— tu función se está ejecutando en una fase del ciclo de vida distinta de la que asumías, y ese desajuste explica por casi todos los bugs de “el estado todavía no está listo”. Y si la pila no llega hasta tu código de origen sino que empieza en algo de la plataforma, el flujo cruzó una frontera asíncrona y necesitas la pila asíncrona completa para verlo entero. La costumbre que aprovecha todo esto es sencilla y merece convertirse en reflejo: antes de mirar las variables, lee la pila entera de abajo arriba y pregúntate si el recorrido es el que esperabas. En una fracción sorprendente de los casos, la respuesta a esa pregunta es el bug, y ni siquiera has tenido que mirar un valor.