wandres.dev
SOURCES III · Scope, watch y el async stack

Depurar cuando la pila pasa por una librería

El flujo completo para bugs donde tu código y el de un framework se entrelazan: combinar ignorados, breakpoints de DOM, pilas asíncronas y bisección.

⏱ 17 min

El caso más frecuente en una aplicación moderna no es un bug en tu código ni un bug en la librería: es un desajuste entre lo que tú esperas que haga la librería y lo que hace. El código de ambos lados es correcto por separado y el resultado es incorrecto. Diagnosticar eso exige combinar las herramientas de los tres niveles anteriores en un orden concreto, y ese orden es el contenido de esta lección.

🎯 Al terminar esta lección sabrás
  • Determinar si un bug está en tu código, en la librería o en la interfaz entre ambos.
  • Aplicar la bisección sobre la frontera entre tu código y el de la librería.
  • Investigar el código de una librería minificada sin source maps.
  • Reconocer los tres patrones de desajuste más frecuentes con frameworks.

La primera pregunta: de quién es el problema

Antes de nada, una bisección barata que ahorra mucho tiempo. Hay tres posibilidades y se distinguen con dos comprobaciones.

Comprobación uno: ¿la librería recibe lo que crees que recibe? Pon un breakpoint —o mejor, un logpoint— en la última línea tuya antes de entregarle el control. Inspecciona los argumentos. Si lo que le entregas no es lo que pretendías, el bug es tuyo y ya has terminado.

Comprobación dos: ¿la librería devuelve lo que documenta? Un logpoint en la primera línea tuya después de recuperar el control. Si el valor no coincide con lo que la documentación promete, o bien la librería tiene un bug, o bien la estás usando de una forma que no contemplaba.

Si ambas comprobaciones dan resultados correctos y el comportamiento sigue mal, tienes el tercer caso: un desajuste de expectativas sobre cuándo ocurren las cosas, que es el más difícil y el más frecuente.

Los tres desajustes típicos

El desajuste temporal. Tu código lee un valor antes de que el framework lo haya actualizado, o mide una dimensión antes de que el DOM se haya pintado, o llama a una API antes de que el componente esté montado. El síntoma característico es un valor correcto que llega tarde: el segundo render tiene lo que el primero no tenía.

El diagnóstico es poner logpoints en los puntos del ciclo de vida y en tu lectura, y leer el orden en que salen. Casi siempre la respuesta salta a la vista: tu lectura sale antes que la actualización.

El desajuste de identidad. Tu código pasa un objeto o una función que se recrea en cada render, y la librería lo compara por referencia para decidir si algo cambió. El resultado es trabajo infinito: un efecto que se ejecuta en cada render porque su dependencia siempre es nueva, o un memo que nunca acierta.

El diagnóstico es una expresión vigilada con una comparación de identidad contra el valor anterior, o un console.count en el efecto sospechoso. Un contador que sube sin parar es la confirmación.

El desajuste de propiedad. Dos partes del código creen ser dueñas del mismo estado. El caso canónico es un componente controlado y no controlado a la vez, donde tu código escribe un valor y la librería lo sobrescribe, o al revés. El síntoma es un valor que parpadea o que vuelve solo a lo que era.

El diagnóstico es el breakpoint de modificación de atributos sobre el nodo, que enseña las dos escrituras y sus dos pilas. Ver las dos pilas juntas suele terminar la investigación.

El flujo completo

El orden que funciona, aplicable a cualquier bug donde tu código y una librería se entrelacen.

Paso uno: configura la lista de ignorados. Sin esto, todo lo demás es incómodo. Con la librería ignorada, las pilas y el paso a paso hablan de tu código.

Paso dos: pon el punto de parada en el efecto observable, no en el código. Si el síntoma es visual, breakpoint de DOM. Si es una petición, breakpoint de red. Si es un error, pausa en excepciones. Este es el paso donde se ahorra la mayor parte del tiempo.

Paso tres: lee la pila entera, incluida la asíncrona. Ignora los marcos superiores si son de la librería. Busca el primer marco tuyo, y luego busca el tramo asíncrono que dice qué originó todo.

Paso cuatro: bisecciona sobre la frontera. Con el punto de origen y el punto del efecto identificados, la pregunta es en cuál de los saltos entre tu código y la librería se torció. Un logpoint en cada frontera y una sola reproducción dan la secuencia completa.

Paso cinco: si hace falta, entra en la librería. Sácala temporalmente de la lista de ignorados, o usa el paso genérico. En este punto ya sabes exactamente en qué función entrar, que es muy distinto de entrar a ciegas.

Investigar una librería minificada

Cuando llegas al paso cinco y no hay source map, hay tres técnicas.

Formatea. El botón de pretty print convierte una línea de doce mil caracteres en código con estructura. Los nombres siguen siendo de una letra, pero el flujo se lee.

Busca cadenas literales. Los nombres de variable se minifican; las cadenas, no. Un mensaje de error, un nombre de propiedad de la API pública, un identificador de tipo: todo eso sobrevive intacto y se busca con la búsqueda global. Es la forma más rápida de encontrar la función responsable dentro de un bundle enorme.

Sigue desde la pila. Si has llegado hasta ahí desde un breakpoint, el marco te deja en el sitio exacto sin necesidad de buscar nada.

Y hay una cuarta que se olvida a menudo: mirar si existe la versión sin minificar. Muchos paquetes publican ambas, y cambiar temporalmente el script cargado por la versión de desarrollo, mediante un override local, convierte una investigación desagradable en una normal.

Verificar la hipótesis en la librería

Cuando crees haber encontrado el problema dentro del código de la librería, hay una comprobación que evita el error de reportar un bug que no existe: modifica el comportamiento en vivo y comprueba que el síntoma desaparece.

Con un breakpoint condicional en el punto sospechoso y una expresión que cambie el valor, o editando la variable en el panel de scope, se puede probar la corrección sin tocar nada. Si el síntoma se va, tu hipótesis es correcta. Si no se va, has encontrado otra cosa.

Y si resulta ser un bug real de la librería, un override local con la corrección aplicada permite seguir trabajando mientras se resuelve por el cauce normal.

El bug casi nunca está en la librería, y saber eso ahorra un día entero

Existe un sesgo tan consistente que casi merece llamarse ley: cuando un desarrollador lleva una hora atascado con código propio que interactúa con una librería, la hipótesis que aparece es que la librería tiene un bug. Y es casi siempre falsa. Las librerías populares tienen decenas de miles de usuarios ejecutando su código a diario, así que un fallo en la ruta principal se descubre en horas, no en meses. Si tu caso fuera un bug de verdad, habría un informe abierto, y buscarlo cuesta un minuto, que es exactamente lo que hay que hacer antes de invertir una tarde. Lo que sí es enormemente frecuente, y lo que casi siempre resulta ser, son tres cosas distintas que se confunden con un bug de la librería. La primera: estás usando la API de una forma que la documentación no contempla, y el comportamiento resultante no está definido; el código de la librería es correcto y tu uso está fuera de contrato. La segunda: hay dos versiones de la librería cargadas a la vez, cosa sorprendentemente común con dependencias transitivas, y las instancias de una no funcionan con el código de la otra; el síntoma característico es una comprobación de tipo que falla sobre un objeto que evidentemente es de ese tipo, y la comprobación es buscar el nombre del paquete en el árbol de Sources y contar cuántas copias hay. La tercera, la más frecuente de todas: el bug está en tu código y la librería solo lo hace visible, porque le entregas un dato mal formado o la llamas en un momento en que su estado interno no es el que su contrato exige. La consecuencia práctica es una regla de orden que ahorra muchísimo tiempo: antes de entrar a depurar el código de una librería, invierte diez minutos en los tres descartes —busca el informe, cuenta las copias cargadas, y verifica en su documentación que tu uso está contemplado. En la gran mayoría de los casos, uno de los tres da la respuesta, y en los que no, entras a depurar sabiendo que tienes motivos, que es una posición muy distinta de entrar por frustración.