The Anatomy of a Race Condition

While contributing to Music Blocks — Sugar Labs’ music-education app — I traced a console error to a positioning patch that silently failed on slow loads. Inside that one small fix live seven JavaScript mechanisms every working engineer leans on daily. This lab teaches each one with code you can poke. Budget 20–30 minutes.
1. The cast of characters
Music Blocks has a search box for finding programming blocks. When you type, a dropdown of suggestions appears. Three facts set up everything that follows:
• The dropdown is not inside the search box — it’s a separate element attached to the end of the page. Something must calculate where to draw it.
• The search widget is created late in startup, after palettes and instrument sounds load — anywhere from 3 to 60+ seconds in.
• The positioning code lived in a different file that could not know when the widget would exist — so it guessed, with a timer.
2. Measuring the screen: getBoundingClientRect()
Every element can tell you where it currently sits in the viewport (the visible window), via element.getBoundingClientRect(). The fix calls it on the search box every time the dropdown renders, then pins the dropdown to those live numbers.
Notice the numbers change as you drag. That’s the key property: the rect is a fresh measurement, not a stored value. Code that re-measures can never be stale; code that measures once can.
3. Two coordinate systems: absolute vs fixed
CSS gives you two different answers to “where should this element go?” position: absolute pins it to a spot in the page — scroll the page, and it rides along. position: fixed pins it to a spot on the glass — the page moves underneath it.
Music Blocks’ search palette floats on the glass (like fixed), but jQuery UI’s default dropdown is positioned in the page (like absolute). While nothing moves they agree. The moment the page scrolls or the layout shifts, they disagree — and the dropdown visibly detaches. Try it:
What you just saw: in default mode the dropdown scrolled away with the page content, because its position was computed once in page coordinates. In patched mode it re-measures the box on the glass and uses fixed, so scrolling cannot separate them. This is the entire visual payoff of the fix — and why a previous contributor wrote the patch in the first place.
4. jQuery in ten minutes of honesty
jQuery predates most modern browser APIs. Its core trick: jQuery("#search") (or $("#search")) finds elements and wraps them in an object bristling with methods. jQuery UI adds ready-made widgets — autocomplete among them — that store their live instance on the element via .data(). The demos below run against a 20-line reimplementation so you can see there’s no magic:
$("#demo-box").length$("#demo-box").css("background", ...)$("#demo-box").data("ui-autocomplete")$("#demo-box").autocomplete({ source: [...] }) // create widget$("#nope").lengthRun snippet 3 before and after snippet 4. Before: undefined — no widget exists yet. After: an object. That exact check — “does .data("ui-autocomplete") return anything?” — is what the old polling code asked twenty times, and what the fixed function asks once, at the right moment. Snippet 5 shows jQuery’s quiet failure mode: selecting nothing isn’t an error, just an empty wrapper — which is why guard clauses check .length.
5. Monkey-patching & idempotency
The positioning fix works by replacing a method on the live widget: save the original _renderMenu, substitute a wrapper that calls the original and then adds positioning. That’s monkey-patching — powerful, and dangerous in one specific way: wrap twice and your addition runs twice. Every layer of wrapping is another onion skin that never comes off.
With no guard, each patch adds a ring, and one render positions the dropdown once per ring — harmless-looking today, a performance leak and a debugging nightmare later. The guarded version stamps a flag on the instance (_mbPositionFixApplied) and refuses to re-wrap: call it a hundred times, one ring. A function that’s safe to call repeatedly is called idempotent — and a reviewer on the competing PR asked for precisely this guard.
6. Why setTimeout(..., 0) isn’t zero
Inside the patch there’s a strange line: the positioning happens in setTimeout(…, 0). Zero milliseconds — so, immediately? No. JavaScript runs one thing at a time; a timeout callback, even at 0ms, is placed in a queue and runs only after the current work finishes. That’s the point: let jQuery UI completely finish laying out the menu, then apply our coordinates so nothing overwrites them.
log("A: draw the suggestion list");
setTimeout(() => log("C: pin dropdown to the box"), 0);
log("B: jQuery UI finishes its own layout");runs now (call stack)
queued for after
The order is always A, B, C — C last, even at “0ms.” Engineers say the callback runs “on the next tick.” One mechanism, two very different uses in this story: here it’s used correctly (defer until current work completes); in the next section it was used as a guess about the future — and that’s where it breaks.
7. The race, playable
The old code needed the widget to exist before patching it. Its strategy: start 1 second after page load, check every half-second, give up after 20 tries (~11s total) and log an error. The app, meanwhile, creates the widget whenever startup finishes — 3 seconds on a warm cache, 40+ on classroom Wi-Fi. Two independent timelines, no coordination: a race condition. Drag the slider; press play.
At 3s the poller wins and everything works — which is exactly why this bug shipped: it passed on every developer’s fast machine. At 12s and beyond the poller dies before the widget is born, and no code path ever revisits the question. Now switch to fix: event-driven and replay any speed. There is no poller lane at all: the patch is called by the same code that creates the widget, so it fires at the exact moment of creation — the race isn’t won, it’s deleted.
8. The actual fix, annotated
Everything above compresses into this diff:
// BEFORE — jquery-setup.js guessed with a timer:
setTimeout(fixAutocompletePosition, 1000); // start guessing
// ...checks 20x, every 500ms, then:
console.error("Autocomplete setup failed…"); // gives up forever
// AFTER — jquery-setup.js just defines a capability:
window.fixSearchAutocompletePosition = function () {
if (!$search.length || !$search.data("ui-autocomplete")) return false; // §4 guards
if (!instance || instance._mbPositionFixApplied) return false; // §5 idempotency
instance._renderMenu = function (ul, items) { // §5 monkey-patch
originalRenderMenu.call(this, ul, items);
setTimeout(() => { // §6 next tick
const rect = searchInput.getBoundingClientRect(); // §2 fresh measure
dropdown.style.position = "fixed"; // §3 on-glass coords
...
}, 0);
};
instance._mbPositionFixApplied = true;
return true;
};
// AFTER — search-controller.js calls it at the birthplace of the widget:
$search.autocomplete({ ... }); // widget is created HERE…
if (typeof window.fixSearchAutocompletePosition === "function") {
window.fixSearchAutocompletePosition(); // …so patch it HERE — §7, race deleted
}Every line now traces back to a mechanism you’ve poked with your own hands.
9. Check yourself
1 · The dropdown never appeared at all for slow-loading users. True or false?
2 · Why not just increase the retries from 20 to 200?
3 · What breaks if you call the unguarded patch function twice?
_renderMenu — including the previous wrapper. Two calls means every render runs the positioning logic twice; N calls, N times. The guard (_mbPositionFixApplied stamped on the instance) makes the second call return false without touching anything — idempotency.4 · Why does the patch position the dropdown inside setTimeout(…, 0)?
5 · A competing PR inlines the same 15 lines in two files instead of sharing one function. Name one cost and one benefit.
window global. Neither answer is stupid; engineering is choosing which cost you’d rather carry. (The fix I submitted — PR #8099 — chose the single guarded function.)