wandres.dev
SOURCES I · Depurar con breakpoints

Breakpoints de línea, de columna y la lista que los gobierna

Poner puntos de parada con precisión, incluidos los de varias sentencias en una misma línea, y gestionar la colección sin borrarla.

⏱ 14 min

Un breakpoint es una instrucción al motor de JavaScript para que se detenga antes de ejecutar cierta posición del código y ceda el control al depurador. Suena simple y tiene más matices de los que parece: hay líneas donde no se puede poner, hay líneas con varios puntos posibles, hay breakpoints que el motor mueve a otro sitio sin avisar, y hay una lista central que casi nadie usa y que evita el ciclo destructivo de poner y borrar continuamente.

🎯 Al terminar esta lección sabrás
  • Colocar breakpoints en la posición exacta, incluidas varias sentencias en la misma línea.
  • Explicar por qué un breakpoint se desplaza a una línea distinta de la que pulsaste.
  • Gestionar la colección de puntos de parada activándolos y desactivándolos en lugar de borrarlos.
  • Usar la desactivación global antes de medir cualquier cosa.

Poner y quitar

Un clic en el número de línea del margen izquierdo pone un breakpoint; otro clic lo quita. El marcador aparece como una etiqueta azul sobre el número.

Con la tecla de opción o alt mantenida, el clic crea un breakpoint desactivado en lugar de activo, lo cual es cómodo cuando estás preparando varios puntos para una sesión y no quieres que salten todavía.

Y con el botón derecho sobre el número de línea aparece el menú que da acceso a las variantes: condicional, logpoint, y las opciones de edición.

Breakpoints de columna

Una línea puede contener varias posiciones donde detenerse. El caso típico es una cadena de llamadas.

const resultado = datos.filter(esValido).map(normalizar).reduce(acumular, {});

Ahí hay al menos cuatro sitios interesantes: antes de todo, y en cada uno de los tres callbacks. Un breakpoint de línea se detiene solo en el primero.

Al poner un breakpoint en una línea con varias posiciones posibles, las DevTools dibujan marcadores en forma de flecha dentro de la línea, uno por cada punto disponible. Pulsando en cada uno se activa o desactiva el breakpoint en esa posición concreta. Los activos se pintan de un color distinto de los disponibles.

Esto es imprescindible en dos escenarios: código minificado sin source map, donde toda una función cabe en una línea, y código moderno con expresiones encadenadas, que es prácticamente todo.

💡
Tip

Un breakpoint en el callback de un map se detiene una vez por elemento. Con un array de mil elementos eso es inmanejable, y ahí es donde el breakpoint condicional del capítulo siguiente deja de ser un lujo y pasa a ser la única forma de trabajar.

Por qué el breakpoint se mueve

Si pones un breakpoint en una línea en blanco, en un comentario, en una declaración de variable sin inicializar, o en cualquier posición donde no haya código ejecutable, el motor lo desplaza a la siguiente posición válida y el marcador aparece ahí. No es un fallo: es que la posición que pediste no existe en el código compilado.

Hay un segundo caso más desconcertante. En código con source map, la posición del fuente original se traduce a una posición del código generado, y si el mapa es impreciso —cosa frecuente con varias capas de transformación— la traducción puede caer en un sitio que al volver a traducir hacia atrás produce otra línea. El resultado es un breakpoint que aparece dos o tres líneas más abajo de donde lo pusiste.

La comprobación es directa: desactiva los source maps en las preferencias y mira dónde cae el breakpoint en el código real. Si ahí está en el sitio correcto, el problema es el mapa y no el depurador.

Y hay un tercer caso, el más raro: en funciones que el motor ha optimizado agresivamente, la posición puede resolverse de forma inesperada. Es infrecuente y suele resolverse recargando.

La lista de breakpoints

El subpanel de breakpoints lista todos los que tienes puestos, agrupados por fichero, con el fragmento de código de cada línea. Cada uno tiene una casilla, y hay una casilla global arriba.

Es la parte del depurador que más cambia la forma de trabajar y la que menos gente usa, porque el hábito extendido es poner un breakpoint, usarlo y borrarlo. Ese ciclo tiene un coste oculto: la próxima vez que ese módulo dé problemas, hay que volver a encontrar la línea.

La alternativa es acumular. Un proyecto en el que trabajas a diario acaba con veinte o treinta breakpoints estratégicos —la entrada del enrutador, el punto donde se normaliza la respuesta de la API, el manejador de errores global, el reductor del estado— todos desactivados. Cuando aparece un problema en una zona, activas los tres o cuatro que le corresponden y los vuelves a desactivar al terminar. El coste de arranque de cada sesión de depuración baja a cero.

⚠️
Cuidado

Los breakpoints se guardan por URL. Si tu entorno de desarrollo cambia de puerto, o si trabajas con URLs que incluyen un identificador de sesión, los breakpoints no se reconocen y aparecen duplicados o desaparecen. Es una de las razones para tener un origen estable en desarrollo.

La desactivación global

La casilla superior del subpanel —o el botón correspondiente en la barra del depurador, o el atajo Cmd+F8 y Ctrl+F8— desactiva todos los breakpoints de golpe sin borrarlos.

Tiene dos usos y ambos importan.

El primero es medir. Un depurador conectado con breakpoints activos hace que el motor renuncie a optimizaciones, y eso invalida cualquier medición de rendimiento. Desactivar antes de grabar un perfil es parte del protocolo, no una recomendación.

El segundo es usar la aplicación. Cuando tienes veinte breakpoints puestos y necesitas navegar hasta cierto estado para reproducir un bug, la navegación se convierte en una tortura de pausas. Desactivas, llegas al estado, reactivas, y sigues.

El caso de la sentencia debugger

La palabra clave debugger en el código produce el mismo efecto que un breakpoint: si las DevTools están abiertas, la ejecución se detiene ahí. Si no lo están, no hace nada.

Tiene dos ventajas sobre un breakpoint del panel. Sobrevive a los cambios del fichero, porque va dentro. Y funciona en contextos donde poner un breakpoint es incómodo: dentro de una plantilla, en código generado dinámicamente, en un eval.

Y tiene dos inconvenientes graves que la hacen inadecuada como herramienta habitual. Modifica el código, con el riesgo evidente de que llegue a producción, donde detendrá la aplicación de cualquiera que tenga el panel abierto. Y exige recargar para ponerla y otra recarga para quitarla, que es exactamente el ciclo lento que los breakpoints eliminan.

El uso legítimo es el de un debugger con retardo desde la consola para congelar estados fugaces, ya visto en el nivel de Elements, y el de instrumentar código que no se puede alcanzar de otra forma.

El breakpoint que pones antes de que exista el código

Hay una situación que parece un callejón sin salida y tiene una solución elegante que muy poca gente conoce: cómo poner un breakpoint en código que todavía no se ha cargado. El caso aparece constantemente en aplicaciones con carga diferida por rutas, con módulos que se importan dinámicamente, y sobre todo cuando el bug ocurre durante el arranque, antes de que te dé tiempo a abrir el panel y encontrar el fichero. Hay tres respuestas, en orden de potencia. La primera es que los breakpoints se recuerdan por URL, así que si consigues poner uno una vez, sigue ahí en la siguiente carga y se activará en cuanto el fichero se cargue, aunque sea diez segundos después del arranque; basta con haber llegado una vez a esa ruta. La segunda es el breakpoint de instrumentación de primera sentencia de script, que vive en la categoría de scripts de los breakpoints de evento y que detiene la ejecución en la primera línea de cada script que se cargue; es ruidoso, y es la única forma de tomar el control antes de que corra nada de una página que no controlas. Y la tercera, la que resuelve el caso general y la que más se usa una vez conocida: abrir las DevTools y recargar con la pausa preparada, combinando la casilla de pausar en excepciones con un breakpoint de evento de carga del documento. Con eso, la ejecución se detiene antes de que la aplicación arranque, y desde ahí puedes navegar a cualquier fichero, ponerle breakpoints, y continuar. La idea general que unifica las tres: el depurador no exige que el código exista para tomar el control; exige que exista un momento anterior en el que puedas detenerte, y en el navegador siempre hay uno.