[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-abort-controller-async-cancellation":32,"code:js:true:ufjq21":183,"code:js:true:189ukeu":184,"code:js:true:17ljd9g":185,"code:js:true:19pxoy":186},[4,20],{"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","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","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.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"title":35,"subtitle":36,"excerpt":37,"coverUrl":38,"locale":13,"readingMinutes":39,"publishedAt":40,"viewCount":39,"likeCount":19,"commentCount":19,"author":41,"vertical":46,"topic":47,"tags":50,"_count":62,"playground":36,"body":63,"bodyMd":174,"seo":175,"translationGroupId":177,"thread":178,"assessments":180,"translations":181,"quiz":36},"019fe65f-e7b8-7175-a185-34775c370bcf","abort-controller-async-cancellation","Your fetch() Is Still Running After the User Left",null,"Stale requests, race conditions in search boxes, state updates on unmounted components — these are all the same bug: async work you forgot to cancel. AbortController is the fix.","\u002Fmedia\u002Fcovers\u002Fabort-controller-async-cancellation.png",4,"2026-07-05T09:12:47.873Z",{"id":42,"name":43,"username":44,"avatarUrl":36,"headline":45},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":48,"name":49},"javascript","JavaScript",[51,53,56,59],{"slug":48,"name":52,"color":36},"Javascript",{"slug":54,"name":55,"color":36},"webdev","Webdev",{"slug":57,"name":58,"color":36},"frontend","Frontend",{"slug":60,"name":61,"color":36},"react","React",{"assessments":19},{"blocks":64,"version":173},[65,69,72,76,79,82,85,88,91,97,100,103,106,110,113,116,120,123,127,130,133,137,140,143,146,149,152,155,158,161,164],{"id":66,"html":67,"type":68},"b1","\u003Cp>When you fire a \u003Ccode>fetch()\u003C\u002Fcode> and the component that triggered it unmounts, the request keeps going. The server still processes it. When the response arrives, it calls back into whatever JavaScript it finds — a stale closure, a dead state setter, a global store that has already moved on. React&#39;s &quot;Can&#39;t perform a state update on an unmounted component&quot; warning is the polite version of this. The silent version is worse: results from an old query overwriting the current UI.\u003C\u002Fp>","paragraph",{"id":70,"html":71,"type":68},"b2","\u003Cp>These aren&#39;t mysterious race conditions. They&#39;re the predictable result of starting async work and never telling it to stop.\u003C\u002Fp>",{"id":73,"html":74,"text":74,"type":75,"level":31},"b3","The race condition hiding in every search box","heading",{"id":77,"html":78,"type":68},"b4","\u003Cp>The search input is the clearest example. The user types &quot;reac&quot;, your debounce fires a request. Before it lands, they finish typing &quot;react&quot; and you fire another. Two requests, in flight at the same time, and no guarantee about which one finishes first.\u003C\u002Fp>",{"id":80,"html":81,"type":68},"b5","\u003Cp>If the &quot;reac&quot; request happens to be slower — network jitter, a cache miss, a heavier result set — it will land after &quot;react&quot; and overwrite the correct results with the wrong ones. The bug reproduces maybe one time in twenty on a local dev server, and consistently in production on a slow connection.\u003C\u002Fp>",{"id":83,"html":84,"type":68},"b6","\u003Cp>The fix isn&#39;t smarter debouncing. It&#39;s cancelling the previous request when a new one starts.\u003C\u002Fp>",{"id":86,"html":87,"text":87,"type":75,"level":31},"b7","AbortController in plain terms",{"id":89,"html":90,"type":68},"b8","\u003Cp>\u003Ccode>AbortController\u003C\u002Fcode> is a browser-native API for cancelling async work. You create a controller, pass its \u003Ccode>signal\u003C\u002Fcode> to \u003Ccode>fetch()\u003C\u002Fcode>, and call \u003Ccode>controller.abort()\u003C\u002Fcode> to cancel. If the response hasn&#39;t arrived yet, the fetch promise rejects with an \u003Ccode>AbortError\u003C\u002Fcode>.\u003C\u002Fp>",{"id":92,"code":93,"type":94,"language":95,"highlight":96},"b9","const controller = new AbortController();\n\nfetch('\u002Fapi\u002Fsearch?q=react', { signal: controller.signal })\n  .then(res => res.json())\n  .then(data => setResults(data))\n  .catch(err => {\n    if (err.name === 'AbortError') return; \u002F\u002F expected — not a real error\n    setError(err);\n  });\n\n\u002F\u002F Somewhere else, when we no longer need this request:\ncontroller.abort();","code","js",[],{"id":98,"html":99,"type":68},"b10","\u003Cp>Two things to internalize: \u003Ccode>signal\u003C\u002Fcode> is how the controller knows which request to cancel, and \u003Ccode>AbortError\u003C\u002Fcode> is intentional — catching and ignoring it is correct behavior, not a smell.\u003C\u002Fp>",{"id":101,"html":102,"text":102,"type":75,"level":31},"b11","The React pattern: cancel in the cleanup function",{"id":104,"html":105,"type":68},"b12","\u003Cp>\u003Ccode>useEffect\u003C\u002Fcode>&#39;s cleanup runs when the component unmounts \u003Cem>and\u003C\u002Fem> before the effect re-runs due to a dependency change. That makes it the natural place to cancel.\u003C\u002Fp>",{"id":107,"code":108,"type":94,"language":95,"highlight":109},"b13","useEffect(() => {\n  const controller = new AbortController();\n\n  fetch(`\u002Fapi\u002Fsearch?q=${query}`, { signal: controller.signal })\n    .then(res => res.json())\n    .then(data => setResults(data))\n    .catch(err => {\n      if (err.name !== 'AbortError') setError(err);\n    });\n\n  return () => controller.abort();\n}, [query]);",[],{"id":111,"html":112,"type":68},"b14","\u003Cp>When \u003Ccode>query\u003C\u002Fcode> changes, React runs the cleanup (aborting the in-flight request) before starting the next effect (firing a new one). The stale &quot;reac&quot; request is cancelled the moment &quot;react&quot; is typed — it will never call back into state.\u003C\u002Fp>",{"id":114,"html":115,"type":68},"b15","\u003Cp>When the component unmounts — navigation, modal close, conditional render — the cleanup fires and the pending request dies. No stale updates, no console warnings.\u003C\u002Fp>",{"id":117,"html":118,"text":119,"type":75,"level":31},"b16","It&#39;s not just for fetch()","It's not just for fetch()",{"id":121,"html":122,"type":68},"b17","\u003Cp>The \u003Ccode>signal\u003C\u002Fcode> property on an \u003Ccode>AbortController\u003C\u002Fcode> isn&#39;t coupled to \u003Ccode>fetch\u003C\u002Fcode>. Any API that accepts an \u003Ccode>AbortSignal\u003C\u002Fcode> can participate, and you can check \u003Ccode>signal.aborted\u003C\u002Fcode> in your own async loops:\u003C\u002Fp>",{"id":124,"code":125,"type":94,"language":95,"highlight":126},"b18","async function processItems(items, signal) {\n  for (const item of items) {\n    if (signal.aborted) return; \u002F\u002F bail out before the next iteration\n    await processOne(item);\n  }\n}\n\nconst controller = new AbortController();\nprocessItems(largeList, controller.signal);\n\n\u002F\u002F A cancel button, a timeout, a navigation — any of these can call:\ncontroller.abort();",[],{"id":128,"html":129,"type":68},"b19","\u003Cp>Check \u003Ccode>signal.aborted\u003C\u002Fcode> at each yield point. This pattern replaces global boolean flags and ad-hoc &quot;is this still relevant?&quot; tracking with a single, standard primitive that any caller can trigger.\u003C\u002Fp>",{"id":131,"html":132,"type":68},"b20","\u003Cp>For timeouts specifically, \u003Ccode>AbortSignal.timeout(ms)\u003C\u002Fcode> is cleaner than the manual approach:\u003C\u002Fp>",{"id":134,"code":135,"type":94,"language":95,"highlight":136},"b21","\u002F\u002F Auto-aborts after 5 seconds — no controller to hold, no setTimeout to clear\nfetch('\u002Fapi\u002Fslow-endpoint', { signal: AbortSignal.timeout(5000) })\n  .catch(err => {\n    if (err.name === 'TimeoutError') handleTimeout();\n  });",[],{"id":138,"html":139,"type":68},"b22","\u003Cp>Note that timed-out requests throw \u003Ccode>TimeoutError\u003C\u002Fcode>, not \u003Ccode>AbortError\u003C\u002Fcode> — they&#39;re distinct cases, and it&#39;s useful to handle them differently.\u003C\u002Fp>",{"id":141,"html":142,"text":142,"type":75,"level":31},"b23","What about React Query, SWR, or TanStack?",{"id":144,"html":145,"type":68},"b24","\u003Cp>Data-fetching libraries handle this for you. React Query wires up an \u003Ccode>AbortController\u003C\u002Fcode> under the hood and cancels outgoing requests when a query key changes or a component unmounts. Understanding the pattern still matters: it&#39;s why the library behaves correctly, and you&#39;ll need the manual version any time you write custom \u003Ccode>fetch\u003C\u002Fcode> logic outside the library&#39;s boundaries.\u003C\u002Fp>",{"id":147,"html":148,"text":148,"type":75,"level":31},"b25","The takeaway",{"id":150,"html":151,"type":68},"b26","\u003Cp>Async code doesn&#39;t stop running because you stopped caring about it. A \u003Ccode>fetch()\u003C\u002Fcode> in flight is a real resource — a server-side computation, a response that will arrive and call something — and if nothing cancels it, it will complete against whatever state it finds.\u003C\u002Fp>",{"id":153,"html":154,"type":68},"b27","\u003Cp>\u003Ccode>AbortController\u003C\u002Fcode> is the correct primitive for this. The cost is three lines: create a controller, pass \u003Ccode>signal\u003C\u002Fcode> to \u003Ccode>fetch\u003C\u002Fcode>, return \u003Ccode>controller.abort\u003C\u002Fcode> from the cleanup. The return is eliminating an entire class of subtle, non-deterministic bugs.\u003C\u002Fp>",{"id":156,"html":157,"type":68},"b28","\u003Cp>If you&#39;ve ever filed a race condition as &quot;can&#39;t reliably reproduce,&quot; there&#39;s a good chance the unfiled fix is a missing \u003Ccode>controller.abort()\u003C\u002Fcode>.\u003C\u002Fp>",{"id":159,"type":160},"b29","divider",{"id":162,"html":163,"type":68},"b30","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":165,"type":166,"items":167,"ordered":18},"b31","list",[168,169,170,171,172],"⭐ \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>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>",1,"When you fire a `fetch()` and the component that triggered it unmounts, the request keeps going. The server still processes it. When the response arrives, it calls back into whatever JavaScript it finds — a stale closure, a dead state setter, a global store that has already moved on. React's \"Can't perform a state update on an unmounted component\" warning is the polite version of this. The silent version is worse: results from an old query overwriting the current UI.\n\nThese aren't mysterious race conditions. They're the predictable result of starting async work and never telling it to stop.\n\n## The race condition hiding in every search box\n\nThe search input is the clearest example. The user types \"reac\", your debounce fires a request. Before it lands, they finish typing \"react\" and you fire another. Two requests, in flight at the same time, and no guarantee about which one finishes first.\n\nIf the \"reac\" request happens to be slower — network jitter, a cache miss, a heavier result set — it will land after \"react\" and overwrite the correct results with the wrong ones. The bug reproduces maybe one time in twenty on a local dev server, and consistently in production on a slow connection.\n\nThe fix isn't smarter debouncing. It's cancelling the previous request when a new one starts.\n\n## AbortController in plain terms\n\n`AbortController` is a browser-native API for cancelling async work. You create a controller, pass its `signal` to `fetch()`, and call `controller.abort()` to cancel. If the response hasn't arrived yet, the fetch promise rejects with an `AbortError`.\n\n```js\nconst controller = new AbortController();\n\nfetch('\u002Fapi\u002Fsearch?q=react', { signal: controller.signal })\n  .then(res => res.json())\n  .then(data => setResults(data))\n  .catch(err => {\n    if (err.name === 'AbortError') return; \u002F\u002F expected — not a real error\n    setError(err);\n  });\n\n\u002F\u002F Somewhere else, when we no longer need this request:\ncontroller.abort();\n```\n\nTwo things to internalize: `signal` is how the controller knows which request to cancel, and `AbortError` is intentional — catching and ignoring it is correct behavior, not a smell.\n\n## The React pattern: cancel in the cleanup function\n\n`useEffect`'s cleanup runs when the component unmounts *and* before the effect re-runs due to a dependency change. That makes it the natural place to cancel.\n\n```js\nuseEffect(() => {\n  const controller = new AbortController();\n\n  fetch(`\u002Fapi\u002Fsearch?q=${query}`, { signal: controller.signal })\n    .then(res => res.json())\n    .then(data => setResults(data))\n    .catch(err => {\n      if (err.name !== 'AbortError') setError(err);\n    });\n\n  return () => controller.abort();\n}, [query]);\n```\n\nWhen `query` changes, React runs the cleanup (aborting the in-flight request) before starting the next effect (firing a new one). The stale \"reac\" request is cancelled the moment \"react\" is typed — it will never call back into state.\n\nWhen the component unmounts — navigation, modal close, conditional render — the cleanup fires and the pending request dies. No stale updates, no console warnings.\n\n## It's not just for fetch()\n\nThe `signal` property on an `AbortController` isn't coupled to `fetch`. Any API that accepts an `AbortSignal` can participate, and you can check `signal.aborted` in your own async loops:\n\n```js\nasync function processItems(items, signal) {\n  for (const item of items) {\n    if (signal.aborted) return; \u002F\u002F bail out before the next iteration\n    await processOne(item);\n  }\n}\n\nconst controller = new AbortController();\nprocessItems(largeList, controller.signal);\n\n\u002F\u002F A cancel button, a timeout, a navigation — any of these can call:\ncontroller.abort();\n```\n\nCheck `signal.aborted` at each yield point. This pattern replaces global boolean flags and ad-hoc \"is this still relevant?\" tracking with a single, standard primitive that any caller can trigger.\n\nFor timeouts specifically, `AbortSignal.timeout(ms)` is cleaner than the manual approach:\n\n```js\n\u002F\u002F Auto-aborts after 5 seconds — no controller to hold, no setTimeout to clear\nfetch('\u002Fapi\u002Fslow-endpoint', { signal: AbortSignal.timeout(5000) })\n  .catch(err => {\n    if (err.name === 'TimeoutError') handleTimeout();\n  });\n```\n\nNote that timed-out requests throw `TimeoutError`, not `AbortError` — they're distinct cases, and it's useful to handle them differently.\n\n## What about React Query, SWR, or TanStack?\n\nData-fetching libraries handle this for you. React Query wires up an `AbortController` under the hood and cancels outgoing requests when a query key changes or a component unmounts. Understanding the pattern still matters: it's why the library behaves correctly, and you'll need the manual version any time you write custom `fetch` logic outside the library's boundaries.\n\n## The takeaway\n\nAsync code doesn't stop running because you stopped caring about it. A `fetch()` in flight is a real resource — a server-side computation, a response that will arrive and call something — and if nothing cancels it, it will complete against whatever state it finds.\n\n`AbortController` is the correct primitive for this. The cost is three lines: create a controller, pass `signal` to `fetch`, return `controller.abort` from the cleanup. The return is eliminating an entire class of subtle, non-deterministic bugs.\n\nIf you've ever filed a race condition as \"can't reliably reproduce,\" there's a good chance the unfiled fix is a missing `controller.abort()`.\n\n---\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)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":35,"canonical":176,"description":37},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fabort-controller-async-cancellation","019fe65f-e7b8-7175-a185-391ae82f5a01",{"id":179,"locked":18},"019fe65f-ee8c-7630-a86d-c6590d15412c",[],[182],{"locale":13,"slug":34},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> controller \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">AbortController\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fapi\u002Fsearch?q=react'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> signal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>signal \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">then\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>res \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">then\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setResults\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">catch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>name \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'AbortError'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F expected — not a real error\n\u003C\u002Fspan>    \u003Cspan class=\"shj-func\">setError\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Somewhere else, when we no longer need this request:\n\u003C\u002Fspan>controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">abort\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-func\">useEffect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> controller \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">AbortController\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`\u002Fapi\u002Fsearch?q=\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>query\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> signal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>signal \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">then\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>res \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">then\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setResults\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">catch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>name \u003Cspan class=\"shj-oper\">!==\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'AbortError'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setError\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">abort\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>query\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">processItems\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>items\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> signal\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> item \u003Cspan class=\"shj-kwd\">of\u003C\u002Fspan> items\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>signal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>aborted\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F bail out before the next iteration\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">processOne\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>item\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> controller \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">AbortController\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-func\">processItems\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>largeList\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>signal\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F A cancel button, a timeout, a navigation — any of these can call:\n\u003C\u002Fspan>controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">abort\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Auto-aborts after 5 seconds — no controller to hold, no setTimeout to clear\n\u003C\u002Fspan>\u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fapi\u002Fslow-endpoint'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> signal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">AbortSignal\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">timeout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">5000\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">catch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>name \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'TimeoutError'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-func\">handleTimeout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>"]