Pausar en excepciones: incluidas las que alguien se traga
Las dos casillas de excepciones, por qué la de capturadas parece inútil y es la más valiosa, y cómo hacerla usable en una aplicación real.
Un error que llega a la consola es fácil de investigar: tiene mensaje, tiene pila y tiene enlace al código. El problema son los otros, los que alguien capturó con un catch y convirtió en un return null silencioso, o en un mensaje genérico, o en nada. Esos errores no dejan rastro y producen los bugs más desconcertantes que existen: algo no funciona, no hay ningún mensaje, y el código parece correcto. La casilla de pausar en excepciones capturadas es la respuesta, y es la función de las DevTools con peor fama y mayor valor real.
- Configurar las dos modalidades de pausa en excepciones y saber qué captura cada una.
- Hacer usable la pausa en excepciones capturadas en una aplicación con librerías ruidosas.
- Diagnosticar errores tragados por un
catchsin instrumentar el código. - Reconocer las excepciones normales que forman parte del funcionamiento de una librería.
Las dos casillas
En la barra del depurador hay un botón con un símbolo de pausa dentro de un octógono, y al activarlo aparece una casilla adicional.
Pausar en excepciones no capturadas. La ejecución se detiene cuando una excepción llega hasta arriba sin que nadie la maneje, justo antes de que el navegador la reporte en la consola. Es la modalidad segura y siempre conviene tenerla activa: lo que la consola te habría dado como texto muerto, aquí lo tienes como estado vivo con todas las variables disponibles.
Pausar también en excepciones capturadas. La ejecución se detiene en el punto donde se lanza cualquier excepción, aunque haya un try alrededor que la vaya a manejar perfectamente.
La diferencia entre ambas es enorme en la práctica, y es la razón de que la segunda tenga mala fama.
Al detenerse en una excepción, el panel muestra el objeto de error justo encima de la línea, con su mensaje y su pila. Y la consola evalúa en ese contexto, así que puedes inspeccionar todas las variables que llevaron al error sin haber puesto ningún breakpoint.
Por qué la segunda parece inútil
Actívala en una aplicación real y lo que ocurre es que el depurador se detiene inmediatamente, y otra vez, y otra vez, en código que no es tuyo y que está funcionando correctamente.
La razón es que las excepciones son un mecanismo de control de flujo legítimo y muchas librerías las usan de forma rutinaria. Los casos habituales.
Detección de capacidades: se intenta usar una API dentro de un try y se captura el fallo para elegir la alternativa.
Parseo con reserva: se prueba a interpretar algo como JSON y si falla se trata como texto.
Acceso a almacenamiento: leer localStorage lanza en modo privado en algunos navegadores, y la librería lo captura.
Validación: algunos validadores lanzan y capturan por cada regla evaluada.
Comprobaciones de acceso entre orígenes: acceder a contentDocument de un iframe ajeno lanza, y muchas librerías lo prueban para saber si pueden.
El resultado es que en una aplicación con diez dependencias, activar esta casilla produce decenas de paradas por segundo. La reacción natural es desactivarla y no volver a mirarla, y con eso se pierde la herramienta más potente para una categoría entera de bugs.
Cómo hacerla usable
Hay tres tácticas y con las tres la casilla pasa de inservible a excelente.
Uno: la lista de scripts ignorados. Es la que resuelve el noventa por ciento del ruido. Con las librerías en la lista de ignorados, el depurador no se detiene en excepciones lanzadas dentro de ellas, solo en las de tu código. Eso convierte cientos de paradas en las tres o cuatro que te interesan. Esta sinergia entre dos funciones aparentemente inconexas es la más valiosa de todo el depurador y merece configurarse antes de nada.
Dos: activarla justo antes del momento crítico. No hace falta tenerla puesta toda la sesión. Navega hasta el estado previo al fallo con la casilla desactivada, actívala, y ejecuta la acción que falla. Las paradas se limitan a esa ventana.
Tres: combinarla con un breakpoint de línea. Detente primero en el punto de entrada de la operación sospechosa, y desde ahí activa la casilla y continúa. Igual que la anterior pero con más precisión.
Los casos que resuelve
Caso uno: el catch vacío. El patrón más dañino que existe en JavaScript.
try {
const config = JSON.parse(localStorage.getItem('preferencias'));
aplicar(config);
} catch (e) {}
Si getItem devuelve null, JSON.parse lanza, aplicar nunca se ejecuta, y no hay absolutamente ninguna señal. La aplicación arranca con las preferencias por defecto y nadie sabe por qué. Con la pausa en capturadas activada, la ejecución se detiene en el JSON.parse, y en dos segundos tienes la causa exacta.
Caso dos: el error convertido en mensaje genérico. Un catch que hace mostrarError('Algo ha ido mal'). El usuario ve una frase inútil, tú no ves nada más, y el error original —que decía exactamente qué campo faltaba— se ha perdido. La pausa te detiene donde se lanzó, con el error real.
Caso tres: la promesa rechazada sin manejador. Una promesa que rechaza y a la que nadie le puso catch produce un rechazo no manejado. Se reporta en la consola, pero a menudo con una pila poco útil porque el punto donde se reporta no es donde se originó. La pausa en excepciones lo captura en el origen.
Caso cuatro: el error dentro de un manejador de eventos. Una excepción lanzada dentro de un listener no rompe el resto de la aplicación: el navegador la reporta y sigue. El síntoma es que a partir de cierto punto un manejador deja de hacer la mitad de su trabajo, sin más señal que una línea en la consola que nadie miró.
Caso cinco: el error que solo ocurre en producción. Combinando la pausa en excepciones con los overrides locales, se puede reproducir el fallo con el código de producción y detenerse exactamente donde ocurre, con los source maps de producción aplicados.
Distinguir el ruido de la señal
Cuando la pausa te detiene en algo, tres preguntas deciden si merece atención.
¿Está en mi código o en el de una librería? El fichero lo dice. Si es de librería y no lo has llamado tú directamente, casi siempre es control de flujo interno.
¿Hay un catch cerca que lo maneja de verdad? Mira el código alrededor. Un catch que registra el error, o que devuelve un valor por defecto documentado, es diseño. Un catch vacío es un bug esperando.
¿El mensaje del error tiene sentido con lo que está pasando? Un TypeError sobre una propiedad de undefined en medio de un flujo que debería tener datos es señal. Un error de acceso denegado al leer una cookie de otro dominio es ruido.
Merece la pena detenerse en por qué esta técnica resuelve tanto, y la razón no es técnica sino cultural. El catch vacío, o el que solo hace un console.log, es una de las construcciones más extendidas de la web y una de las más dañinas, y su origen es comprensible: alguien tuvo un error que rompía la página, lo rodeó de un try para que dejara de romperla, y siguió adelante. El problema es que eso no arregla nada: convierte un fallo ruidoso en un fallo silencioso, que es estrictamente peor. Un fallo ruidoso se detecta el mismo día; un fallo silencioso produce comportamientos degradados que nadie relaciona con nada durante meses. Y la consecuencia más grave es de segundo orden: un catch que traga excepciones no distingue entre las que esperaba y las que no. Alguien escribe un try para tolerar que el almacenamiento no esté disponible, y ese mismo try acaba tragándose un TypeError de un refactor de tres años después, en una línea que nada tiene que ver, y el bug resultante es indiagnosticable desde fuera porque no deja rastro alguno. La pausa en excepciones capturadas es el antídoto exacto de esa práctica porque hace visible lo que el código decidió ocultar, y lo hace sin modificar nada y sin necesitar saber dónde están los catch. Hay dos hábitos que se derivan de esto y que van más allá de la herramienta. El primero, cuando depures: si un flujo no llega donde debería y no hay ningún error en la consola, la hipótesis por defecto debería ser que alguien se lo tragó, y la comprobación es activar la casilla. El segundo, cuando escribas: un catch debe capturar un tipo concreto de fallo previsto y hacer algo con él; si no sabes qué excepción esperas ni qué vas a hacer con ella, el try no debería estar ahí. La regla corta que resume ambos: captura lo que sepas manejar, deja subir lo demás.
Abre cualquier aplicación en la que trabajes, configura la lista de ignorados con tus dependencias, y activa la pausa en excepciones capturadas. Navega durante cinco minutos por los flujos principales. Casi con seguridad vas a encontrar entre uno y cinco errores reales que llevaban meses ocurriendo en silencio, y ninguno de ellos aparecía en tu consola ni en tu sistema de monitorización.