Debugging a UI race condition: a real case
A bug that only happened sometimes, only in a certain click order, and what it teaches about debugging shared state on the frontend.
There's a particularly uncomfortable category of bugs: the ones that don't fail every time, only sometimes, and only if the user does things in a certain order. This is the record of one of those cases, and of the process used to find it, not the specific code, but the way of thinking that solved it.
The symptom
In an admin panel with paginated tables and persistent tabs (the state of "which tab you're viewing" was saved to localStorage to survive reloads), a strange report came in: sometimes, when changing pages in a table, the selected tab would jump back to the first one without anyone clicking on it. Not always. Not reproducible with the same three clicks. That's already the first clue of a race condition: if the bug depends on "sometimes" instead of "every time I do X," suspect number one is execution order, not logic.
Ruling out the obvious first
The first instinct was to suspect the localStorage write logic, maybe an incorrect value was being saved. This was checked by reading the value directly from storage at the moment the bug occurred: the saved value was correct. That ruled out the simplest hypothesis and pointed toward something subtler: the value was being saved correctly, but was either being read at the wrong moment, or overwritten after being read correctly.
Finding the real sequence
The next step was to instrument, not guess: log every write and every read of the tab state with a timestamp, and reproduce the flow until a sequence where the bug occurred was captured. The real sequence turned out to be:
- The user changes pages in the table (pagination).
- That page change triggers a re-initialization of a tabs component from a third-party library (jQuery-UI, mounted inside a React context).
- That library, on initialization, writes its own default value into the same state slot the app's own code used to restore the saved tab, but does so asynchronously, after the app's code had already read
localStorageand applied the correct value. - The result: the correct tab was applied first, and milliseconds later, the library's default initialization silently overwrote it.
This wasn't a "the data is wrong" bug. It was a "two systems write to the same place, and whoever writes last wins" bug, and which one writes last depends on pagination, table size, and how long the browser takes at that specific moment. That's why it was intermittent: it didn't depend on logic, it depended on timing.
The lesson
Faced with an intermittent bug, the useful question isn't "which part of the code is wrong?" It's "what two things are competing to write the same state, and which one normally wins?" Framed that way, the fix was simple: force the saved value's restoration to happen after the third-party library finished its own initialization, instead of racing against it.
Instrumenting with timestamps, not step-through debugging, not reading the code more carefully, is what revealed the problem. With race conditions, guessing the cause by reading the code rarely works, because the code is correct in isolation. The problem lives in the interaction between two pieces that, separately, do exactly what they're supposed to.