A Scandal in Modal

I spent a full day dancing with Claude and chasing a test-suite hang that looked like memory pressure but turned out to be a microtask livelock in a hidden settings modal. The real culprit was a
useEffectthat optimistically cleared a stale color theme, got rejected by a missing mock, reverted, and re-triggered itself - 44,570 times in 45 seconds - while every timeout starved silently on the macrotask queue. The biggest component in the app was innocent; the quietest one was guilty. A postmortem has all the technical details; and what follows is the same story retold as it deserved to be told.
Being a Reprint from the Reminiscences of John H. Watson, M.D., Late of the Rogue Software Engineering Corps
It was upon a sweltering Monday in July of the year 2026 that I found my friend Mr. Sherlock Holmes in one of those states of intense concentration which, in a lesser mind, might have been mistaken for paralysis. He sat rigid before a luminous screen in our quarters at 221B Baker Street, his sharp features bathed in the cold glow of a terminal window, one bony finger pressed to his lips, the other hand hovering above the keyboard like a surgeon’s over the incision site.
“Watson,” said he, without turning, “you are about to tell me that I have not eaten since yesterday, that there is a perfectly good lamb chop going cold upon the sideboard, and that you are worried about my health. Spare yourself. I am upon the trail of something far more nourishing.”
I confessed that these were precisely my intentions, and drew up a chair. “What is the case?”
“Murder, Watson. Or rather, something worse than murder - a livelock.”
He pronounced the word with the gravity another man might reserve for arsenic.
“Our client is a certain Inspector Lestrade of the Continuous Integration Division,” Holmes continued, sliding a dossier across the desk. “His test suite - some three hundred files, the accumulated forensic apparatus of an entire application - has been found dead. Hung. Unresponsive. One worker pegged at a hundred percent of the processor, memory climbing past two gigabytes, and not a single timeout firing. The body was discovered this morning during a routine deployment.”
I examined the report with growing unease. “Good heavens, Holmes. It says here the suite passes perfectly when any individual directory is tested in isolation. The failure manifests only when the full assembly runs together.”
“Precisely!” Holmes sprang from his chair with the sudden energy of a terrier sighting a rat. “And it is precisely that fact which has led everyone astray. Lestrade, and before him the entirety of the engineering constabulary, have concluded that this is a case of accumulation - that the suite, like a man who drinks moderately at a dozen public houses in succession, simply collapses under the cumulative weight. Memory pressure, they say. The heap grows fat on jsdom allocations and CodeMirror instances until the garbage collector can no longer keep pace.”
“That sounds entirely reasonable,” I ventured.
Holmes fixed me with a look of withering patience. “It sounds reasonable, Watson, because it is the sort of explanation that a reasonable man reaches when he reasons from the general to the particular rather than from the particular to the general. It is the Grand Unified Theory of people who have not looked at a CPU sample. Lestrade has already deployed three separate countermeasures on the strength of this theory.” He ticked them off on his fingers. “A project split: unit tests here, UI tests there. A sharding script that runs each directory in isolation. And a memory limit on the worker threads. Three elaborate locks fitted to the wrong door.”
“And the true door?”
Holmes smiled - that thin, predatory smile which I had come to associate with the moment when the fog lifted and the quarry stood exposed.
“Come, Watson. Let us examine the body.”
Part the Second: In Which Several Innocents are Accused
Holmes began, as was his custom, by narrowing the field. Through a process he called “exclusion bisection” - which I gathered was a systematic halving of the suspect pool - he had already established that among the three hundred test files, only one was capable of producing the hang: App.test.tsx, a file of sixty-two tests that exercised the top-level wiring of the application.
“But Holmes,” I objected, “if the file hangs on its own, how can the hang depend on other files running alongside it?”
“It does not, Watson. That is the first false trail. The file hangs reliably in isolation. The appearance of needing the full suite was merely the statistical consequence of always including it in the full suite. When Lestrade ran individual directories, he happened to run App’s directory with fewer files, altering the async timing just enough to dodge the bullet. A race condition, not a dependency.”
He pulled up a verbose test report and pointed to the output. “Now observe. The stall occurs at a different test each time I run - first test two, then test three, then test one. The target moves. This is characteristic of a race-sensitive fault, not a resource-exhaustion fault. A bucket that overflows always overflows at the same level. A duel fought in fog hits a different bystander each time.”
I confess I found the analogy rather vivid.
“But the truly diagnostic clue,” Holmes continued, leaning forward with barely contained excitement, “is here.” He indicated a stream of warning messages in the captured output, each one identical, repeating endlessly like the wallpaper of a madman’s cell:
An update to SettingsModal inside a test was not wrapped in act(...)
An update to SettingsModal inside a test was not wrapped in act(...)
An update to SettingsModal inside a test was not wrapped in act(...)
“When a hung run’s log shows the same warning repeating without end, naming a specific component,” said Holmes, his voice quiet and precise, “that component - or an effect it triggers - is the livelock host. This, Watson, is the footprint in the flower-bed. And yet everyone walked past it, because they were too busy measuring the weight of the shrubbery.”
“SettingsModal,” I read aloud. “What manner of component is this?”
“A modal dialogue for application settings. Mounted unconditionally by the application shell, its visibility governed by a single open property. In every one of these sixty-two tests, the modal sits in the rendered tree, closed but alive - its effects executing silently in the background, like a clerk who continues filing paperwork in a burning building.”
“But surely the chief suspect would be something weightier? This report mentions a ReferenceLibrary - four thousand lines of tests, bibliography scanning, file watchers, a virtualizer -”
“Ah, Watson!” Holmes clapped his hands together. “You have fallen into precisely the same trap as Lestrade. ReferenceLibrary is the obvious suspect. It is the burliest fellow in the room, the one with ink on his cuffs and a motive in his pocket. It was my first suspect as well, and I am not too proud to admit it.”
He paused, and a shadow of something almost like humility crossed his aquiline features. It passed quickly.
“I stubbed it out. Changed nothing. Stubbed the Sidebar that housed it. Nothing. The BottomPanel. The StatusBar. At each step, the hang persisted, though the precise test at which it stalled shifted like a will-o’-the-wisp. And then -” he leaned back, steepling his fingers - “I stubbed SettingsModal.”
“And?”
“Sixty-two tests. Three consecutive runs. Clean as a whistle. I removed the intermediate stubs and left only the SettingsModal stub. Three more runs. Clean. Three full-suite sweeps: forty-six seconds, forty-eight seconds, forty-nine seconds. The mystery, Watson, is solved.”
“But how does a settings dialogue hang an entire test suite?”
Holmes rose and paced before the fireplace - or rather, before the potted fern that occupied the space where a fireplace would have been, had our landlady Mrs. Hudson not converted to electric heating.
“The mechanism,” he said, “is diabolical in its simplicity.”
Part the Third: The Microtask Livelock, or, The Promise That Ate Itself
“Consider, Watson, the architecture of the modern JavaScript event loop. There are macrotasks - timers, I/O callbacks, the broad strokes of program execution - and there are microtasks: promises, state updates, the fine brushwork that executes between each macrotask. The critical property is this: the engine drains the entire microtask queue before proceeding to the next macrotask. If the microtask queue never empties - if each microtask, upon completing, enqueues another - then no macrotask will ever execute again. Timeouts will not fire. The test framework’s watchdog will not bark. The process will spin at full CPU utilisation, doing infinite work and making zero progress.”
“A livelock,” I said, recalling the term from Holmes’s earlier remarks.
“Precisely. Not a deadlock, where everything stops. A livelock, where everything moves and nothing advances. The cruelest trap in concurrent systems: the appearance of industry concealing the reality of futility.”
He pulled up the offending code and traced the chain of causation with his pencil.
“The SettingsModal contained a useEffect - a side-effect hook - added in a recent commit. Its purpose was hygienic: if the user’s saved colour theme was no longer present in the list of available themes, clear it. A reasonable precaution. But observe the execution in the test environment.”
He drew a diagram upon the back of an envelope:
“Step one: the test’s beforeEach sets colorTheme to 'book'. Step two: the mock for list_themes returns an empty array. Step three: the mock for invoke has no case for set_preference, so every persistence call rejects.”
“Now watch the effect. It sees 'book' is not in the empty theme list. Correct - it should not be there. So it calls setRegistryPref to clear it: an optimistic setState to null, followed by a persistence call. The persistence call rejects. The .catch handler, ever dutiful, reverts the state to 'book'. The preferences object changes identity. The effect re-runs. It sees 'book' is not in the empty theme list…”
I felt a chill despite the July heat.
“Ad infinitum,” I whispered.
“I placed a counter on the setRegistryPref call,” said Holmes, with the grim satisfaction of a coroner confirming cause of death. “In a single forty-five-second hung run, it recorded forty-four thousand, five hundred and seventy calls. All for colorTheme. All to null. All reverted. A Sisyphean loop of promise and rejection, each cycle spawning the next as a microtask, starving the event loop of oxygen.”
“And the wandering stall point?”
“Elementary. Different tests configured the mockInvoke differently. Any test whose mock happened to include a resolving set_preference case would survive, because the persistence would succeed, the revert would never fire, and the loop would not ignite. The hang always landed on whichever test lacked that case - and since different stub configurations shifted the async interleaving, the landing zone moved.”
“But the effect was present in every test -”
“In every test that rendered <App />, yes. Sixty-two renderings. Sixty-two live SettingsModals, each one a loaded revolver with its hammer cocked, waiting only for the right async timing to discharge.”
Part the Fourth: The Arrest, and Some Remarks on the Nature of Hidden Components
Inspector Lestrade, upon receiving Holmes’s findings, expressed the mixture of gratitude and chagrin that was his customary response to being rescued from his own conclusions.
“But Mr. Holmes,” said he, turning his hat in his hands, “the memory was climbing. Two gigabytes! Surely that supports the accumulation theory?”
“A red herring of the highest quality,” Holmes replied. “The memory climbed because of the livelock, not the reverse. Forty-four thousand state updates per run, each creating new objects, new Map instances, new Object.entries iterations under the VM-context property getters - the garbage collector was scavenging frantically, but the allocations outpaced it. The CPU sample was unambiguous: Builtins_RunMicrotasks dominated the profile, hot on Map construction and object enumeration. That is a livelock signature, Inspector, not slow accumulation. You diagnosed the symptom and missed the disease.”
Lestrade departed with the fix: a stub for SettingsModal in App.test.tsx, and - Holmes insisted upon this point with unusual vehemence - a structural repair to the production code itself.
“The stub is a tourniquet, Watson, not a suture. The loop exists in the shipping application. If a user’s preferences file becomes unwritable - a full disk, a permissions error - the identical livelock will seize their settings modal in production. And there is a launch race besides: if preferences load before the theme list initialises, the effect sees an empty list and persists away the user’s valid saved theme.”
The full repair, which Holmes detailed in a subsequent monograph (filed as Issue the Eight Hundred and Ninety-First), was elegant in its economy:
The stale-theme cleanup was relocated from the modal into the theme store itself, gated on a themesLoaded flag so that an empty list before initialisation would merely deactivate, never clear. Persistence became fire-and-forget: the in-memory null was correct for the session; a persistence failure would simply retry on next launch. No feedback edge was possible. The catch handler was tightened so that it would only revert if the store still held the optimistic value, eliminating the stale-revert-clobbers-newer-write edge for every registry setting. And the modal itself was made lazy: mounted only when opened, so that no modal effects would run while the dialogue was closed.
“The deepest lesson,” Holmes remarked that evening, as he filled his pipe and settled into the basket chair, “is one of epistemology. Every engineer who examined this case - and I include my earlier self in that company - began with a hypothesis derived from the external symptoms: it hangs when everything runs together, therefore cumulative weight. They then constructed elaborate workarounds consistent with that hypothesis: sharding, memory limits, project splits. Each workaround appeared to help, because each one altered the async timing enough to dodge the race condition. And each one added lasting complexity to the codebase while leaving the actual fault untouched.”
“You are describing,” I said, “the danger of treating symptoms.”
“I am describing the danger of reasoning from category rather than from evidence. ‘The suite is large, therefore the problem is scale.’ ‘The component is heavy, therefore the component is guilty.’ The ReferenceLibrary, Watson - four thousand lines of tests, bibliography scanning, file watchers - was innocent. The culprit was a modal that nobody was looking at, because it weighed nothing and did nothing visible. Weight-based heuristics pointed at the wrong component.”
He struck a match and held it to the bowl.
“Always check the CPU sample before reaching for the sharding script. Always read the repeating warnings before blaming the largest file. And never, ever, trust a component that sits hidden in the DOM, running its effects in silence. The quietest tenant in the building is not always the most trustworthy.”
He drew upon his pipe and regarded the ceiling with an air of profound satisfaction.
“The game, Watson, was microtasks. And for forty-four thousand, five hundred and seventy iterations, nobody noticed.”
Published in The Strand Magazine, Digital Edition, July 2026
Editor’s note: The SettingsModal has since been placed under permanent surveillance. It has not re-offended.