[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-fetch-race-condition-stale-responses":44,"search-suggestions":60,"quiz-article-fetch-race-condition-stale-responses":108,"path-membership-fetch-race-condition-stale-responses":281},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a1972e-eafe-7186-8afa-f7ad21233d58","fetch-race-condition-stale-responses","PRACTICE_QUIZ","Race conditions in fetch responses","Check what actually stuck from the search-box race condition post: why debouncing doesn't fix it, how the requestId guard decides what to render, and where the same pattern shows up outside search boxes.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",170,{"slug":66,"name":67,"articles":68},"javascript","Javascript",137,{"slug":70,"name":71,"articles":72},"frontend","Frontend",94,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",69,{"slug":78,"name":79,"articles":80},"css","Css",50,{"slug":82,"name":83,"articles":84},"performance","Performance",30,{"slug":86,"name":87,"articles":88},"typescript","Typescript",25,{"slug":90,"name":91,"articles":92},"react","React",20,{"slug":94,"name":95,"articles":96},"browser","Browser",18,{"slug":98,"name":99,"articles":96},"programming","Programming",{"slug":101,"name":102,"articles":103},"node","Node",15,{"slug":105,"name":106,"articles":107},"html","Html",11,{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":113,"publishedAt":114,"viewCount":88,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":267,"seo":268,"translationGroupId":271,"series":52,"podcastUrl":52,"verticalId":5,"thread":272,"assessments":274,"translations":277,"quiz":279},"01a1972e-eaa8-706f-bb0e-2c61b0faeef0","Stop Shipping Search Boxes With a Race Condition","Type fast into a search box wired to fetch(), and the result on screen isn't from your last keystroke — it's from whichever request happened to finish last. That's a race condition, and the fix is a one-line guard.","\u002Fmedia\u002Fcovers\u002Ffetch-race-condition-stale-responses.png",6,"2026-11-07T06:21:08.821Z",{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":122},"JavaScript",[124,125,126,127],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":82,"name":83,"color":52},{"slug":98,"name":99,"color":52},{"assessments":129},1,{"slug":46,"title":131},"race condition — interactive playground",{"blocks":133,"version":129},[134,138,141,145,148,154,157,160,164,167,170,173,176,179,182,186,189,192,195,198,201,204,207,210,214,217,225,228,231,234,237,240,243,246,249,252,255,258,261],{"id":135,"html":136,"type":137},"b1","\u003Cp>Type &quot;react&quot; into a search box, watch it fetch results, then quickly backspace and type &quot;vue&quot; instead. For half a second, the dropdown shows \u003Ccode>react\u003C\u002Fcode> results under the word \u003Ccode>vue\u003C\u002Fcode>. Then it corrects itself, or it doesn&#39;t, and either way nobody files a bug report because by the time you&#39;d screenshot it, it&#39;s gone.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>This happened at a filter UI I worked on — product search, nothing exotic, one \u003Ccode>fetch()\u003C\u002Fcode> call per keystroke. QA flagged it as &quot;occasionally shows stale results, can&#39;t reproduce.&quot; That phrase should make you suspicious immediately. &quot;Can&#39;t reproduce&quot; usually means &quot;reproduces under conditions I&#39;m not controlling for&quot; — and the condition here was network speed, which nobody controls on purpose.\u003C\u002Fp>",{"id":142,"html":143,"text":143,"type":144,"level":31},"b3","The fix that made it rarer, not gone","heading",{"id":146,"html":147,"type":137},"b4","\u003Cp>The instinct, reasonably, was to stop firing so many requests:\u003C\u002Fp>",{"id":149,"code":150,"type":151,"language":152,"highlight":153},"b5","let timer;\nsearchInput.addEventListener(\"input\", (e) => {\n  clearTimeout(timer);\n  timer = setTimeout(() => runSearch(e.target.value), 250);\n});\n\nasync function runSearch(query) {\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n  const data = await res.json();\n  renderResults(data);\n}","code","js",[],{"id":155,"html":156,"type":137},"b6","\u003Cp>Debounce the input, wait 250ms of silence before firing. Fewer requests, fewer chances to collide — the bug got rarer. It did not go away. On a slow connection, or with a server that happens to answer a short query faster than a long one, two debounced requests can still be in flight at once, and they can still finish in the wrong order.\u003C\u002Fp>",{"id":158,"html":159,"type":137},"b7","\u003Cp>\u003Cstrong>Before you keep reading — what do you think &quot;wrong order&quot; actually means here?\u003C\u002Fstrong> It&#39;s not that the requests go out of order. It&#39;s that they \u003Cem>come back\u003C\u002Fem> out of order, and nothing in the code above checks which one it&#39;s looking at before painting it on screen.\u003C\u002Fp>",{"id":161,"html":162,"text":163,"type":144,"level":31},"b8","The response you render isn&#39;t the one you think it is","The response you render isn't the one you think it is",{"id":165,"html":166,"type":137},"b9","\u003Cp>Here&#39;s the part that doesn&#39;t show up in a quick read of the code: \u003Ccode>fetch()\u003C\u002Fcode> makes no promise about finish order. You can send request A, then request B a moment later, and B&#39;s response can arrive first — a shorter query, a warmer cache entry, a server that happens to be less loaded for that specific request. Nothing is broken. This is just what concurrent network calls do.\u003C\u002Fp>",{"id":168,"html":169,"type":137},"b10","\u003Cp>\u003Ccode>renderResults(data)\u003C\u002Fcode> in the snippet above has no idea which query the data it just received belongs to. It just paints whatever shows up last. If A was \u003Ccode>&quot;react&quot;\u003C\u002Fcode> and B was \u003Ccode>&quot;vue&quot;\u003C\u002Fcode>, and A&#39;s response happens to land after B&#39;s, the box ends up showing react results while the input says \u003Ccode>vue\u003C\u002Fcode>. The bug isn&#39;t in your fetch call. It&#39;s in the fact that you&#39;re treating &quot;the last response that arrived&quot; as if it were &quot;the response to my last request&quot; — and those are only the same thing when nothing is racing.\u003C\u002Fp>",{"id":171,"html":172,"type":137},"b11","\u003Cp>That&#39;s the whole bug, named precisely: it&#39;s a \u003Cstrong>race condition\u003C\u002Fstrong> — two async operations whose outcome depends on which one happens to finish first, and &quot;happens to&quot; is exactly the word that should worry you. It&#39;s not about when things run on the main thread. It&#39;s about the gap between \u003Cem>starting\u003C\u002Fem> an operation and \u003Cem>acting on its result\u003C\u002Fem>, and anything else that can also act in that gap.\u003C\u002Fp>",{"id":174,"html":175,"text":175,"type":144,"level":31},"b12","The fix: stop trusting arrival order, track identity instead",{"id":177,"html":178,"type":137},"b13","\u003Cp>You can&#39;t force responses to come back in order — that&#39;s a property of the network, not your code. What you \u003Cem>can\u003C\u002Fem> do is make sure that when a response arrives, you check whether it&#39;s still the one you care about before you act on it.\u003C\u002Fp>",{"id":180,"html":181,"type":137},"b14","\u003Cp>The smallest version of that is a counter:\u003C\u002Fp>",{"id":183,"code":184,"type":151,"language":152,"highlight":185},"b15","let latestRequestId = 0;\n\nasync function runSearch(query) {\n  const requestId = ++latestRequestId;\n\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n  const data = await res.json();\n\n  if (requestId !== latestRequestId) {\n    return; \u002F\u002F a newer search started while this one was in flight — drop it\n  }\n\n  renderResults(data);\n}",[],{"id":187,"html":188,"type":137},"b16","\u003Cp>Every call to \u003Ccode>runSearch\u003C\u002Fcode> claims the next number and remembers it. When its response finally arrives — however late, however out of order — it checks whether it&#39;s still the most recent request anyone asked for. If a newer one started in the meantime, this one&#39;s result is thrown away, silently, no matter how long it took to get here. The one request that&#39;s still &quot;latest&quot; when its answer arrives is the only one allowed to touch the DOM.\u003C\u002Fp>",{"id":190,"html":191,"type":137},"b17","\u003Cp>Three lines. No library, no state management rewrite. The requests still race each other exactly as before — you&#39;ve just stopped letting the loser win.\u003C\u002Fp>",{"id":193,"html":194,"type":137},"b18","\u003Cp>If you&#39;d rather cancel the stale request outright instead of just ignoring its answer, \u003Ccode>AbortController\u003C\u002Fcode> does that — \u003Ccode>controller.abort()\u003C\u002Fcode> on the previous request before firing a new one, and a caught \u003Ccode>AbortError\u003C\u002Fcode> instead of the manual counter check. That trims a network call that would otherwise complete for nothing, which is worth it on anything heavier than a search box. The counter guard above is the version to reach for when you don&#39;t control the request layer, or just want the smallest possible fix.\u003C\u002Fp>",{"id":196,"html":197,"type":137},"b19","\u003C!-- playground:start -->",{"id":199,"html":200,"text":200,"type":144,"level":31},"b20","🎮 Try it yourself",{"id":202,"html":203,"type":137},"b21","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-race-condition-stale-responses\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":205,"html":206,"type":137},"b22","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":208,"html":209,"type":137},"b23","\u003C!-- playground:end -->",{"id":211,"html":212,"text":213,"type":144,"level":31},"b24","It&#39;s not just search boxes","It's not just search boxes",{"id":215,"html":216,"type":137},"b25","\u003Cp>Once you see this shape, you start noticing it everywhere two async calls can overlap and only one should win:\u003C\u002Fp>",{"id":218,"type":219,"items":220,"ordered":18},"b26","list",[221,222,223,224],"\u003Cstrong>Tabs in a dashboard\u003C\u002Fstrong> — click &quot;Revenue,&quot; then quickly click &quot;Users.&quot; If Revenue&#39;s data is bigger and arrives after Users&#39;, the Users tab briefly shows revenue numbers.","\u003Cstrong>Infinite scroll with filters\u003C\u002Fstrong> — change a filter mid-scroll, and an in-flight page request for the \u003Cem>old\u003C\u002Fem> filter lands after the new one does.","\u003Cstrong>Autosave\u003C\u002Fstrong> — type, pause, type again, pause. Two save requests, and if the first one is slow, it can overwrite the second one&#39;s result on the server.","\u003Cstrong>Optimistic UI rollback\u003C\u002Fstrong> — a retry fired after a timeout can resolve after the original request you thought had failed, and now you&#39;ve &quot;rolled back&quot; a success.",{"id":226,"html":227,"type":137},"b27","\u003Cp>Every one of these has the same fix: attach an identity to the thing you&#39;re waiting for, and check it&#39;s still current before you act on the answer. React Query and SWR build this in for you automatically — stale responses are dropped before they ever reach your UI, which is a large part of why &quot;just use a data-fetching library&quot; is good advice, not laziness. Redux Toolkit&#39;s \u003Ccode>createAsyncThunk\u003C\u002Fcode> doesn&#39;t drop them for you, but it hands you the exact piece the guard above needs: every dispatched thunk carries a unique \u003Ccode>meta.requestId\u003C\u002Fcode>, so instead of inventing your own counter you compare that id in your reducer. But knowing the three-line version means you&#39;re never stuck when you&#39;re not using one of those, which is most code that isn&#39;t a component&#39;s data layer.\u003C\u002Fp>",{"id":229,"html":230,"text":230,"type":144,"level":31},"b28","Guard the identity, not the order",{"id":232,"html":233,"type":137},"b29","\u003Cp>The lesson generalizes past fetch: whenever you start an async operation and plan to act on its result later, something else can start in the meantime and finish first. You can&#39;t make the network, the event loop, or another developer&#39;s code behave in request order for you. You can make sure that when a result shows up, you check whether it&#39;s still the one you&#39;re waiting for before you let it touch anything a user can see.\u003C\u002Fp>",{"id":235,"html":236,"type":137},"b30","\u003Cp>Go find the oldest \u003Ccode>fetch()\u003C\u002Fcode> or \u003Ccode>setTimeout\u003C\u002Fcode> callback in your codebase that fires more than once per user action — a search box, a save button, a polling loop. Does anything check that the response it just got is still the one it wants? If the honest answer is no, you&#39;ve found a race condition that just hasn&#39;t been unlucky yet.\u003C\u002Fp>",{"id":238,"html":239,"type":137},"b31","\u003C!-- quiz:start -->",{"id":241,"html":242,"text":242,"type":144,"level":31},"b32","🧠 Test yourself",{"id":244,"html":245,"type":137},"b33","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-race-condition-stale-responses\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":247,"html":248,"type":137},"b34","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":250,"html":251,"type":137},"b35","\u003C!-- quiz:end -->",{"id":253,"type":254},"b36","divider",{"id":256,"html":257,"type":137},"b37","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":259,"html":260,"type":137},"b38","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":262,"type":219,"items":263,"ordered":18},"b39",[264,265,266],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Type \"react\" into a search box, watch it fetch results, then quickly backspace and type \"vue\" instead. For half a second, the dropdown shows `react` results under the word `vue`. Then it corrects itself, or it doesn't, and either way nobody files a bug report because by the time you'd screenshot it, it's gone.\n\nThis happened at a filter UI I worked on — product search, nothing exotic, one `fetch()` call per keystroke. QA flagged it as \"occasionally shows stale results, can't reproduce.\" That phrase should make you suspicious immediately. \"Can't reproduce\" usually means \"reproduces under conditions I'm not controlling for\" — and the condition here was network speed, which nobody controls on purpose.\n\n## The fix that made it rarer, not gone\n\nThe instinct, reasonably, was to stop firing so many requests:\n\n```js\nlet timer;\nsearchInput.addEventListener(\"input\", (e) => {\n  clearTimeout(timer);\n  timer = setTimeout(() => runSearch(e.target.value), 250);\n});\n\nasync function runSearch(query) {\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n  const data = await res.json();\n  renderResults(data);\n}\n```\n\nDebounce the input, wait 250ms of silence before firing. Fewer requests, fewer chances to collide — the bug got rarer. It did not go away. On a slow connection, or with a server that happens to answer a short query faster than a long one, two debounced requests can still be in flight at once, and they can still finish in the wrong order.\n\n**Before you keep reading — what do you think \"wrong order\" actually means here?** It's not that the requests go out of order. It's that they *come back* out of order, and nothing in the code above checks which one it's looking at before painting it on screen.\n\n## The response you render isn't the one you think it is\n\nHere's the part that doesn't show up in a quick read of the code: `fetch()` makes no promise about finish order. You can send request A, then request B a moment later, and B's response can arrive first — a shorter query, a warmer cache entry, a server that happens to be less loaded for that specific request. Nothing is broken. This is just what concurrent network calls do.\n\n`renderResults(data)` in the snippet above has no idea which query the data it just received belongs to. It just paints whatever shows up last. If A was `\"react\"` and B was `\"vue\"`, and A's response happens to land after B's, the box ends up showing react results while the input says `vue`. The bug isn't in your fetch call. It's in the fact that you're treating \"the last response that arrived\" as if it were \"the response to my last request\" — and those are only the same thing when nothing is racing.\n\nThat's the whole bug, named precisely: it's a **race condition** — two async operations whose outcome depends on which one happens to finish first, and \"happens to\" is exactly the word that should worry you. It's not about when things run on the main thread. It's about the gap between *starting* an operation and *acting on its result*, and anything else that can also act in that gap.\n\n## The fix: stop trusting arrival order, track identity instead\n\nYou can't force responses to come back in order — that's a property of the network, not your code. What you *can* do is make sure that when a response arrives, you check whether it's still the one you care about before you act on it.\n\nThe smallest version of that is a counter:\n\n```js\nlet latestRequestId = 0;\n\nasync function runSearch(query) {\n  const requestId = ++latestRequestId;\n\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n  const data = await res.json();\n\n  if (requestId !== latestRequestId) {\n    return; \u002F\u002F a newer search started while this one was in flight — drop it\n  }\n\n  renderResults(data);\n}\n```\n\nEvery call to `runSearch` claims the next number and remembers it. When its response finally arrives — however late, however out of order — it checks whether it's still the most recent request anyone asked for. If a newer one started in the meantime, this one's result is thrown away, silently, no matter how long it took to get here. The one request that's still \"latest\" when its answer arrives is the only one allowed to touch the DOM.\n\nThree lines. No library, no state management rewrite. The requests still race each other exactly as before — you've just stopped letting the loser win.\n\nIf you'd rather cancel the stale request outright instead of just ignoring its answer, `AbortController` does that — `controller.abort()` on the previous request before firing a new one, and a caught `AbortError` instead of the manual counter check. That trims a network call that would otherwise complete for nothing, which is worth it on anything heavier than a search box. The counter guard above is the version to reach for when you don't control the request layer, or just want the smallest possible fix.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-race-condition-stale-responses\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## It's not just search boxes\n\nOnce you see this shape, you start noticing it everywhere two async calls can overlap and only one should win:\n\n- **Tabs in a dashboard** — click \"Revenue,\" then quickly click \"Users.\" If Revenue's data is bigger and arrives after Users', the Users tab briefly shows revenue numbers.\n- **Infinite scroll with filters** — change a filter mid-scroll, and an in-flight page request for the *old* filter lands after the new one does.\n- **Autosave** — type, pause, type again, pause. Two save requests, and if the first one is slow, it can overwrite the second one's result on the server.\n- **Optimistic UI rollback** — a retry fired after a timeout can resolve after the original request you thought had failed, and now you've \"rolled back\" a success.\n\nEvery one of these has the same fix: attach an identity to the thing you're waiting for, and check it's still current before you act on the answer. React Query and SWR build this in for you automatically — stale responses are dropped before they ever reach your UI, which is a large part of why \"just use a data-fetching library\" is good advice, not laziness. Redux Toolkit's `createAsyncThunk` doesn't drop them for you, but it hands you the exact piece the guard above needs: every dispatched thunk carries a unique `meta.requestId`, so instead of inventing your own counter you compare that id in your reducer. But knowing the three-line version means you're never stuck when you're not using one of those, which is most code that isn't a component's data layer.\n\n## Guard the identity, not the order\n\nThe lesson generalizes past fetch: whenever you start an async operation and plan to act on its result later, something else can start in the meantime and finish first. You can't make the network, the event loop, or another developer's code behave in request order for you. You can make sure that when a result shows up, you check whether it's still the one you're waiting for before you let it touch anything a user can see.\n\nGo find the oldest `fetch()` or `setTimeout` callback in your codebase that fires more than once per user action — a search box, a save button, a polling loop. Does anything check that the response it just got is still the one it wants? If the honest answer is no, you've found a race condition that just hasn't been unlucky yet.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-race-condition-stale-responses\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":110,"canonical":269,"description":270},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-race-condition-stale-responses","Type fast into a search box wired to fetch(), and the result on screen isn't from your last keystroke — it's from whichever request happened to finish last. That's a race condition","01a1972e-eaa8-706f-bb0e-33560f500b10",{"id":273,"locked":18},"01a1972e-eada-727a-a9c3-66bd05c2b88c",[275],{"id":45,"slug":46,"title":48,"_count":276},{"questions":51},[278],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":280,"questionCount":51},{"questions":51},[]]