Why We Put Race Conditions in the Spec, Not the Bug Tracker
Most bugs we get called in to fix after another team has shipped aren't logic errors. The logic is fine. What's missing is an answer to a question nobody wrote down: what happens if this runs twice, out of order, or not at all?
The five questions we ask before writing a form
Every form, every fetch, every piece of state that can change while a request is in flight gets the same five questions before a line of implementation code is written:
- What if the user submits twice? A double click, a slow network making a button feel unresponsive so it gets clicked again — if the second submission isn't blocked, you get two records, or worse, an error toast for a request that actually succeeded.
- What if the response arrives out of order? Type two search queries quickly and the first request can resolve after the second. Without a sequence guard, the UI shows results for the query you already abandoned.
- What if the component unmounts mid-request? A route change, a modal closing, a tab switch to a different page — the fetch doesn't know to stop, and a
setStatecall on an unmounted component either warns loudly or silently corrupts state elsewhere. - What if the list is empty, or has exactly one item? The zero case and the one case break more carousels, dropdowns, and pagination controls than any other input.
- What if the network fails partway? Not "the request fails" — partway. A multi-step form that's saved steps 1 and 2 server-side but fails on step 3 needs a defined recovery path, not a generic error screen that discards everything.
What this looks like in code
On this project, the cost calculator's submission flow is a direct answer to questions 1 and 3: every submit creates an AbortController, stores it in a ref, and a cleanup effect aborts it if the component unmounts before the request resolves. The reducer's SUBMIT_START action is a no-op if a submission is already in flight — so the double-click case never reaches the network at all.
The testimonials carousel answers question 4 directly: it renders correctly whether there's one page or six, and the previous/next controls simply don't render when there's nothing to page through, rather than existing in a disabled-but-visible limbo state.
None of this is exotic. It's the difference between treating "handle the edge case" as a code review comment versus a design constraint that shapes the state shape from the first commit. The second approach is slower on day one and faster on every day after.