Debugging de una race condition de UI: un caso real
Un bug que solo aparecía a veces, en cierto orden de clics, y lo que enseña sobre depurar estado compartido en el frontend.
Hay una categoría de bugs particularmente incómoda: los que no fallan siempre, solo a veces, y solo si el usuario hace las cosas en cierto orden. Este es el registro de uno de esos casos, y del proceso para encontrarlo, no del código específico, sino de la forma de pensar que lo resolvió.
El síntoma
En un panel administrativo con tablas paginadas y pestañas persistentes (el estado de "qué pestaña estás viendo" se guardaba en localStorage para sobrevivir recargas), apareció un reporte extraño: a veces, al cambiar de página en una tabla, la pestaña seleccionada volvía a la primera sin que nadie hiciera clic en ella. No siempre. No de forma reproducible con los mismos tres clics. Esa es ya la primera señal de una race condition: si el bug depende de "a veces" en vez de "siempre que hago X", el sospechoso número uno es el orden de ejecución, no la lógica.
Descartar lo obvio primero
El primer instinto fue sospechar de la lógica de guardado en localStorage, quizás se estaba escribiendo un valor incorrecto. Se verificó leyendo el valor directamente del storage en el momento del bug: el valor guardado era correcto. Eso descartó la hipótesis más simple y señaló hacia algo más sutil: el valor se guardaba bien, pero se leía en el momento equivocado, o se sobrescribía después de leerse correctamente.
Encontrar la secuencia real
El siguiente paso fue instrumentar, no adivinar: loggear cada escritura y cada lectura del estado de pestaña con una marca de tiempo, y reproducir el flujo hasta capturar una secuencia donde el bug ocurriera. La secuencia real resultó ser:
- El usuario cambia de página en la tabla (paginación).
- Ese cambio de página dispara una re-inicialización de un componente de pestañas de una librería de terceros (jQuery-UI, montada dentro de un contexto React).
- Esa librería, al inicializarse, escribe su propio valor por defecto en el mismo espacio de estado que el código propio usaba para restaurar la pestaña guardada, pero lo hace de forma asíncrona, después de que el código propio ya había leído
localStoragey aplicado el valor correcto. - El resultado: la pestaña correcta se aplicaba primero, y milisegundos después, la inicialización por defecto de la librería la sobrescribía silenciosamente.
No era un bug de "el dato está mal". Era un bug de "dos sistemas escriben al mismo lugar, y quien escribe último gana", y cuál de los dos escribe último depende de la paginación, del tamaño de la tabla, y de cuánto tarda el navegador en ese momento específico. Por eso era intermitente: no dependía de la lógica, dependía de timing.
La lección
Frente a un bug intermitente, la pregunta útil no es "¿qué parte del código está mal?", sino "¿qué dos cosas están compitiendo por escribir el mismo estado, y en qué orden gana cada una normalmente?". Una vez planteada así, la solución fue simple: forzar que la restauración del valor guardado ocurriera después de que la librería de terceros terminara su propia inicialización, en vez de competir con ella.
La instrumentación con timestamps, no el debugger paso a paso, no leer el código con más atención, fue lo que reveló el problema. Con race conditions, adivinar la causa leyendo el código rara vez funciona, porque el código en sí es correcto en aislamiento. El problema vive en la interacción entre dos piezas que, por separado, hacen exactamente lo que deberían.