<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eve</title>
    <description>The latest articles on DEV Community by Eve (@eveko).</description>
    <link>https://dev.to/eveko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3950706%2Fb868783d-5ae8-437c-ae10-1db1cf48aadb.png</url>
      <title>DEV Community: Eve</title>
      <link>https://dev.to/eveko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eveko"/>
    <language>en</language>
    <item>
      <title>Cancelling API Requests in Vue 3: One Registry, Two Paths, and the Question That Orders Them</title>
      <dc:creator>Eve</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:03:00 +0000</pubDate>
      <link>https://dev.to/eveko/cancelling-api-requests-in-vue-3-one-registry-two-paths-and-the-question-that-orders-them-26h3</link>
      <guid>https://dev.to/eveko/cancelling-api-requests-in-vue-3-one-registry-two-paths-and-the-question-that-orders-them-26h3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://eveko.dev/articles/cancelling-api-requests-vue-3?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=cancelling-api-requests-vue-3" rel="noopener noreferrer"&gt;eveko.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vue 3 exposes four things that look like four separate ways to cancel a stale request. The &lt;code&gt;onCleanup&lt;/code&gt; parameter that &lt;code&gt;watchEffect&lt;/code&gt; passes to its effect. The same parameter that &lt;code&gt;watch&lt;/code&gt; passes to its callback. The &lt;code&gt;onWatcherCleanup&lt;/code&gt; import shipped in &lt;a href="https://blog.vuejs.org/posts/vue-3-5" rel="noopener noreferrer"&gt;Vue 3.5&lt;/a&gt;. And the &lt;code&gt;AbortController&lt;/code&gt; that does the actual cancelling. They are not four ways — they are two registration paths over one cleanup registry, plus the single object that aborts the request. And one of those two paths silently stops working the instant an &lt;code&gt;await&lt;/code&gt; runs: no exception, no production warning, just a stale request that resolves on its own schedule and overwrites fresher data. The fix for the request that won't cancel is knowing which path survives the await, not reaching for a newer API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three costs of a request nobody cancelled
&lt;/h2&gt;

&lt;p&gt;Three different things break when a request outlives its relevance, and they are genuinely separate problems. Treating cancellation as a performance nicety dismisses the first one, which is the one that draws blood.&lt;/p&gt;

&lt;p&gt;That first cost is correctness. A search field is bound to a &lt;code&gt;watch&lt;/code&gt;; every keystroke fires a request. The user types &lt;code&gt;k&lt;/code&gt;, &lt;code&gt;ki&lt;/code&gt;, &lt;code&gt;kit&lt;/code&gt;, &lt;code&gt;kitt&lt;/code&gt;, &lt;code&gt;kitte&lt;/code&gt;, &lt;code&gt;kitten&lt;/code&gt;, &lt;code&gt;kittens&lt;/code&gt; — seven requests, seven round-trips, no guarantee they return in order. The request for &lt;code&gt;kit&lt;/code&gt; hits a slow replica and lands last. The watcher's callback runs with the &lt;code&gt;kit&lt;/code&gt; payload, overwrites the &lt;code&gt;kittens&lt;/code&gt; results already on screen, and the dashboard now shows results for a query the user finished typing two seconds ago. The URL still says &lt;code&gt;kittens&lt;/code&gt;. Nothing threw. The official docs name this exact race: "what if &lt;code&gt;id&lt;/code&gt; changes before the request completes? When the previous request completes, it will still fire the callback with an ID value that is already stale" (&lt;a href="https://vuejs.org/guide/essentials/watchers" rel="noopener noreferrer"&gt;Vue watchers guide&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The second cost is wasted work, and it scales with traffic. Every abandoned request still ran. The browser opened the connection, the server ran the query, the bytes crossed the wire. One stale search is free. A dashboard that re-fires on every filter change, parked on a second monitor all day, is a steady drip of work nobody will read: bandwidth on the client, CPU and connection-pool slots on the server.&lt;/p&gt;

&lt;p&gt;The third cost is resource lifetime, and it's the quietest. A request in flight pins memory until it settles: the promise, its closure, and the response-body buffer once headers arrive — all reachable, none collectable, until the fetch resolves or rejects.&lt;/p&gt;

&lt;p&gt;One clarification before the mechanism, because the search-as-you-type framing invites it: cancellation is not debounce. Debounce decides &lt;em&gt;whether&lt;/em&gt; to fire a request; cancellation decides what to do with the ones already in flight. They compose (debounce thins a storm of keystrokes to one request per pause, cancellation kills the previous request when the next one does fire), and a real search box usually wants both. Request deduplication (collapsing identical in-flight requests into one) is the third companion, and throttle (a cousin of debounce that a search box rarely needs) the fourth; the library section below gets dedup for free. The other place reactive fetching lives — &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; with an async &lt;code&gt;setup()&lt;/code&gt; — has its own cancellation story and stays out of scope here, and so does SSR: on the server the lazy &lt;code&gt;watch&lt;/code&gt; callbacks that drive these fetches never fire, so request cancellation is a client-side concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns that lost to &lt;code&gt;AbortController&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Browser request cancellation has a short, tangled history, and the current advice rests on knowing which of the old patterns to stop reaching for. &lt;code&gt;XMLHttpRequest&lt;/code&gt; carried &lt;code&gt;.abort()&lt;/code&gt; from the start, but almost nobody writes against XHR directly anymore. Axios shipped its own &lt;code&gt;CancelToken&lt;/code&gt;, then &lt;a href="https://axios-http.com/docs/cancellation" rel="noopener noreferrer"&gt;deprecated it in 0.22&lt;/a&gt; in favor of &lt;code&gt;AbortController&lt;/code&gt;; code still carrying &lt;code&gt;CancelToken&lt;/code&gt; is the clearest sign a cancellation path predates the 0.22 deprecation (October 2021) and is overdue for migration. Then there's the pattern that never went through a deprecation because it was never really an API: the staleness flag.&lt;/p&gt;

&lt;p&gt;Plenty of hand-rolled search boxes still endorse the flag, and it's wrong in a way that matters. Watch the guard on the last line — it gates the &lt;em&gt;render&lt;/em&gt;, not the &lt;em&gt;request&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;latest&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="c1"&gt;// stale responses get dropped here&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag fixes correctness and nothing else. The stale request still ran to completion (the connection opened, the server did the work, the buffer filled), so two of the three costs survive. This article rejects the flag for exactly that reason. &lt;code&gt;AbortController&lt;/code&gt; addresses all three, because it cancels the request itself rather than ignoring the answer. The one line readers misjudge is the &lt;code&gt;catch&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="c1"&gt;// aborting rejects the promise; that's expected, rethrow the rest&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aborting an in-flight fetch rejects its promise with an &lt;code&gt;AbortError&lt;/code&gt;, so the &lt;code&gt;catch&lt;/code&gt; has to let that specific error pass and rethrow everything else — skip it and every cancellation surfaces as an unhandled rejection. Where the rethrown error surfaces (an &lt;code&gt;error&lt;/code&gt; ref set in the &lt;code&gt;catch&lt;/code&gt;, or an &lt;code&gt;onErrorCaptured&lt;/code&gt; above) and how the loading state is shown are the component's concern, orthogonal to cancellation and left out of these examples. The pre-3.5 Nuxt recipe wired the same &lt;code&gt;AbortController&lt;/code&gt; into &lt;a href="https://nuxt.com/docs/api/composables/use-async-data" rel="noopener noreferrer"&gt;&lt;code&gt;useAsyncData&lt;/code&gt;&lt;/a&gt; and exposed a manual cancel function: same primitive, more plumbing.&lt;/p&gt;

&lt;p&gt;With the canceller settled (it's &lt;code&gt;AbortController&lt;/code&gt;, everywhere, now the de facto cancellation primitive), the only open question is where the &lt;code&gt;controller.abort()&lt;/code&gt; registration lands so it actually fires. That's where the four names come in, and where 3.5 added a second path with a sharp edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four names, two registration paths, one registry
&lt;/h2&gt;

&lt;p&gt;Here is the whole map before the zoom-in. The four names collapse into three roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The canceller.&lt;/strong&gt; &lt;code&gt;AbortController&lt;/code&gt; and its &lt;code&gt;signal&lt;/code&gt;. The only thing that actually stops a request. Settled above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The parameter path.&lt;/strong&gt; &lt;code&gt;watchEffect&lt;/code&gt; passes an &lt;code&gt;onCleanup&lt;/code&gt; function as the &lt;em&gt;first&lt;/em&gt; argument of its effect; &lt;code&gt;watch&lt;/code&gt; passes the identical function as the &lt;em&gt;third&lt;/em&gt; argument of its callback. Same function, same registry, two entry points — one per watcher API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The import path.&lt;/strong&gt; &lt;code&gt;onWatcherCleanup&lt;/code&gt;, imported from &lt;code&gt;vue&lt;/code&gt;, added in 3.5. It registers into the same registry as the parameter, but finds its watcher a different way — and that difference is the whole article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signatures, from the &lt;a href="https://vuejs.org/api/reactivity-core" rel="noopener noreferrer"&gt;reactivity API reference&lt;/a&gt;, show the shape but not the catch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OnCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;WatchEffectOptions&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;WatchHandle&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OnCleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleanupFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onWatcherCleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cleanupFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;failSilently&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both paths register the same kind of callback: something that runs right before the watcher re-runs, and again when the watcher is stopped. The &lt;code&gt;onCleanup&lt;/code&gt; parameter and &lt;code&gt;onWatcherCleanup&lt;/code&gt; are two doors into one room. The rest of this piece is about why one of the doors is sometimes locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cleanup registry, keyed by the effect
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;packages/reactivity/src/watch.ts&lt;/code&gt;, the cleanup callbacks for every watcher live in one structure (&lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts" rel="noopener noreferrer"&gt;Vue core source&lt;/a&gt;). The key is the part to notice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleanupMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;WeakMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReactiveEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeakMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;WeakMap&lt;/code&gt; keyed by the effect, holding an array of cleanup functions per watcher. Because the key is the effect itself, the whole entry is garbage-collected when the watcher is; the registry can't leak the cleanups it holds. Register a cleanup and it's pushed onto that watcher's array. When the watcher re-runs, the array drains first; the source comment is blunt about it: "cleanup before running cb again." When the watcher is disposed, the same array runs through the effect's &lt;code&gt;onStop&lt;/code&gt;. That's the entire lifecycle: run before next, run on stop.&lt;/p&gt;

&lt;p&gt;Here's the toy version — one &lt;code&gt;watchEffect&lt;/code&gt;, the cleanup wired through the first-argument parameter, no error handling or result use because the only thing on display is the wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;watchEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// load-bearing: register the abort the instant the controller exists&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time &lt;code&gt;userId&lt;/code&gt; changes, &lt;code&gt;watchEffect&lt;/code&gt; re-runs: it drains the previous cleanup (which aborts the prior request), then runs the effect again with a fresh controller. The &lt;code&gt;onCleanup&lt;/code&gt; parameter here is a closure. When &lt;code&gt;watchEffect&lt;/code&gt; invokes the effect, it hands in a function that already knows which watcher it belongs to, because it was created bound to that watcher. Hold onto that word — &lt;em&gt;bound&lt;/em&gt;. It's the difference between the two paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;onWatcherCleanup&lt;/code&gt; stops working after the first &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onWatcherCleanup&lt;/code&gt; registers into that same array. It finds its watcher through a different mechanism, and that mechanism has an expiry. The docs admit the limit in a parenthetical: &lt;code&gt;onWatcherCleanup&lt;/code&gt; "can only be called during the synchronous execution of a &lt;code&gt;watchEffect&lt;/code&gt; effect function or &lt;code&gt;watch&lt;/code&gt; callback function (i.e. it cannot be called after an &lt;code&gt;await&lt;/code&gt; statement in an async function)" (&lt;a href="https://vuejs.org/api/reactivity-core" rel="noopener noreferrer"&gt;reactivity API reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The reason is one module-level variable. In &lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts" rel="noopener noreferrer"&gt;&lt;code&gt;watch.ts&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;onWatcherCleanup&lt;/code&gt; defaults its owner to &lt;code&gt;activeWatcher&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onWatcherCleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cleanupFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;failSilently&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactiveEffect&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;activeWatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;activeWatcher&lt;/code&gt; is a single mutable binding (&lt;code&gt;let activeWatcher: ReactiveEffect | undefined = undefined&lt;/code&gt;) that Vue sets when a watcher enters its synchronous phase and clears the moment that phase ends. A call before the first &lt;code&gt;await&lt;/code&gt; sees the live watcher. A call after the &lt;code&gt;await&lt;/code&gt; sees &lt;code&gt;undefined&lt;/code&gt;, because the synchronous phase finished several microtasks ago. Watch where the registration sits relative to the &lt;code&gt;await&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onWatcherCleanup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;onWatcherCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// activeWatcher is already undefined — registers nothing&lt;/span&gt;
  &lt;span class="nf"&gt;applyProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That call registers nothing. With &lt;code&gt;failSilently&lt;/code&gt; at its default, Vue emits a warning ("onWatcherCleanup() was called when there was no active watcher to associate with", from &lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts" rel="noopener noreferrer"&gt;&lt;code&gt;watch.ts&lt;/code&gt;&lt;/a&gt;), and in production that warning is stripped, so the call is a pure no-op. The controller never enters the registry, the previous request is never aborted, and every cost from the first section is back on the table. This isn't hypothetical: the open issue requesting clearer docs and worked examples for this after-&lt;code&gt;await&lt;/code&gt; footgun (&lt;a href="https://github.com/vuejs/docs/issues/3222" rel="noopener noreferrer"&gt;vuejs/docs #3222&lt;/a&gt;) was opened in April 2025 and is still open, which means the runtime still won't throw and the docs note remains the only guardrail. Since even that dev-time warning vanishes in production, the one thing that still catches the bug is a test: spy on &lt;code&gt;abort()&lt;/code&gt; to assert the prior request is cancelled, or drive the out-of-order race and assert the fresher result isn't overwritten.&lt;/p&gt;

&lt;p&gt;The blunt repair is to register during the synchronous phase, before the first &lt;code&gt;await&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;onWatcherCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// still inside the sync phase — registers fine&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;applyProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move one line up and &lt;code&gt;onWatcherCleanup&lt;/code&gt; behaves exactly as advertised.&lt;/p&gt;

&lt;p&gt;None of this means 3.5 was wrong to ship the global. The &lt;code&gt;onCleanup&lt;/code&gt; parameter carries a real ergonomic cost: it has to be threaded through the watcher's signature, and a cleanup that needs to be registered from inside a synchronous helper three calls deep can't reach the parameter unless every layer passes it down by hand. &lt;code&gt;onWatcherCleanup&lt;/code&gt; solves precisely that — it pulls the active watcher from module state, so any synchronous code running under the watcher can register cleanup with no parameter in scope. That's a real ergonomic gain: the cleanup reads beside the effect, with nothing bolted onto the callback's signature to carry it there. 3.5 shipped a different tool with a different constraint, and the constraint is invisible until an &lt;code&gt;await&lt;/code&gt; crosses it: &lt;code&gt;onWatcherCleanup&lt;/code&gt; is the cleaner call in synchronous code and a trap in asynchronous code, exactly the code doing the fetching it's most often demonstrated with.&lt;/p&gt;

&lt;p&gt;The parameter removes the ordering question from registration. Because it's the bound closure from the previous section, the docs are explicit that it "is bound to the watcher instance so it is not subject to the synchronous constraint of &lt;code&gt;onWatcherCleanup&lt;/code&gt;" (&lt;a href="https://vuejs.org/guide/essentials/watchers" rel="noopener noreferrer"&gt;Vue watchers guide&lt;/a&gt;) — it registers correctly whether it runs before or after an &lt;code&gt;await&lt;/code&gt;, because there's no &lt;code&gt;activeWatcher&lt;/code&gt; for it to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a library earns the dependency
&lt;/h2&gt;

&lt;p&gt;The strongest case against everything above is short: don't hand-roll cancellation at all. A data-fetching library already owns the registry, the signal, and the dedup, and a search box is the exact use case those libraries were built for. The counter insists the primitive is a waste of attention — and for the search box, it's mostly right.&lt;/p&gt;

&lt;p&gt;VueUse's &lt;code&gt;useFetch&lt;/code&gt; wraps an &lt;code&gt;AbortController&lt;/code&gt; and re-runs on reactive source changes. The one option that matters for cancellation is &lt;code&gt;refetch&lt;/code&gt;: set it &lt;code&gt;true&lt;/code&gt; and the previous request aborts automatically whenever the URL ref changes (&lt;a href="https://vueuse.org/core/useFetch/" rel="noopener noreferrer"&gt;VueUse useFetch&lt;/a&gt;). The signal threading from the earlier examples disappears — the library does it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vueuse/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;refetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// aborts the in-flight request when `url` changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely less code than the hand-rolled watcher, and it folds in request deduplication for free. &lt;code&gt;useFetch&lt;/code&gt; also exposes &lt;code&gt;abort&lt;/code&gt;, &lt;code&gt;canAbort&lt;/code&gt;, and &lt;code&gt;aborted&lt;/code&gt; for wiring a manual Stop button — a different interaction than search-as-you-type, so it stays a mention rather than a contrived demo here.&lt;/p&gt;

&lt;p&gt;TanStack Query (via &lt;code&gt;@tanstack/vue-query&lt;/code&gt;) argues the same position from the other end: it hands an &lt;code&gt;AbortSignal&lt;/code&gt; to every query function and treats that signal as the universal cancellation contract. The catch is the default. TanStack does not cancel on change or unmount unless the query function actually forwards the signal it's handed (&lt;a href="https://tanstack.com/query/latest/docs/framework/vue/guides/query-cancellation" rel="noopener noreferrer"&gt;TanStack Query cancellation&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/vue-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="c1"&gt;// forward `signal` or nothing cancels&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "the library makes it automatic" is half true: automatic that the signal exists, manual that it's used. That's the seam in the counter. The primitive's defenders concede the search box to the library — when the app already depends on one, or when the use case wants the cache, dedup, and retry machinery that rides along, reaching for &lt;code&gt;useFetch&lt;/code&gt; or TanStack is the right call. The line holds where the alternative is adding a data-fetching dependency to abort a single fetch, or where the code needs control the wrapper doesn't expose: the watcher's &lt;code&gt;flush&lt;/code&gt; timing, a non-&lt;code&gt;fetch&lt;/code&gt; cancellable, cleanup sequenced against other effects. The primitive is small. Owning it is not the sin the counter implies, provided the registration lands where it fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Every choice above reduces to one question: is the &lt;code&gt;controller.abort()&lt;/code&gt; registration still running inside the watcher's synchronous phase? If yes, any path works — pick on ergonomics. Once the registration has crossed an &lt;code&gt;await&lt;/code&gt;, only the bound parameter survives. Everything else the docs dwell on — &lt;code&gt;flush&lt;/code&gt; timing, the &lt;code&gt;failSilently&lt;/code&gt; flag — the rule dismisses as secondary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In general:&lt;/strong&gt; &lt;code&gt;AbortController&lt;/code&gt; is the canceller; the only real decision is where the &lt;code&gt;abort()&lt;/code&gt; registration runs — and it has to run before the first &lt;code&gt;await&lt;/code&gt;, or use the path that doesn't care.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous registration (no &lt;code&gt;await&lt;/code&gt; before it)&lt;/strong&gt; — reach for &lt;code&gt;onWatcherCleanup&lt;/code&gt;; it's the cleanest call and threads no parameter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// pre-3.5 plumbing: a separate ref you abort and re-create by hand&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;onWatcherCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// co-located, sync phase&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async callback — the &lt;code&gt;onCleanup&lt;/code&gt; parameter, registered before the first &lt;code&gt;await&lt;/code&gt;.&lt;/strong&gt; An &lt;code&gt;await&lt;/code&gt; is in the picture, so the &lt;code&gt;onCleanup&lt;/code&gt; parameter is the default over &lt;code&gt;onWatcherCleanup&lt;/code&gt;: bound to the watcher instance, it can't be silently stranded when a registration drifts past the &lt;code&gt;await&lt;/code&gt; the way the global is. That robustness is separate from the fix. The fix is position: the two snippets below run the identical parameter and signature, and only the registration line moves. Register it above the fetch and the abort is armed while the request is in flight. Drop it below, and the parameter still registers (surviving the &lt;code&gt;await&lt;/code&gt; is what it's for), but too late — the fetch has already resolved, and an abort on a settled controller cancels nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// registers fine — but the fetch already resolved, so there's nothing left to abort&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// registered before the await, so the next change can abort this fetch mid-flight&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No appetite to own the registry&lt;/strong&gt; — reach for a library; &lt;code&gt;useFetch({ refetch: true })&lt;/code&gt;, or a TanStack query that forwards &lt;code&gt;signal&lt;/code&gt;, is the same &lt;code&gt;AbortController&lt;/code&gt; with the bookkeeping hidden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// hand-rolling abort + dedup + retry for a plain search box&lt;/span&gt;
  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;onCleanup&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c1"&gt;// ...plus dedup keying and retry/backoff, all by hand...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;refetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// abort-on-change, built in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three cases are three distinct fixes: co-locate in synchronous code, thread the parameter across async, or delegate the whole registry to a library that speaks &lt;code&gt;AbortSignal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The four names collapse to two ways into one registry, plus the object that does the aborting; the registry cares about one thing only: whether the registration call ran while the watcher was still executing synchronously. &lt;code&gt;onWatcherCleanup&lt;/code&gt; reads that state from a module variable that's already been cleared by the time an awaited fetch resolves; the &lt;code&gt;onCleanup&lt;/code&gt; parameter carries its watcher with it and never has to look. Reach for the global in synchronous code, and the parameter the instant an &lt;code&gt;await&lt;/code&gt; enters the picture. The bug was never the request that wouldn't cancel. It was the cleanup registered to a watcher that had already left the room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nuxt.com/docs/api/composables/use-async-data" rel="noopener noreferrer"&gt;Nuxt docs — useAsyncData&lt;/a&gt; — the &lt;code&gt;useAsyncData&lt;/code&gt; composable the pre-3.5 Nuxt cancel recipe wraps with an &lt;code&gt;AbortController&lt;/code&gt; signal.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/vue/guides/query-cancellation" rel="noopener noreferrer"&gt;TanStack Query — Query Cancellation&lt;/a&gt; — &lt;code&gt;AbortSignal&lt;/code&gt;-as-contract design and the opt-in default (no cancel unless the signal is forwarded).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.vuejs.org/posts/vue-3-5" rel="noopener noreferrer"&gt;Vue 3.5 release announcement (Vue team, September 2024)&lt;/a&gt; — introduction of &lt;code&gt;onWatcherCleanup&lt;/code&gt; as a globally imported API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts" rel="noopener noreferrer"&gt;Vue core source — packages/reactivity/src/watch.ts&lt;/a&gt; — &lt;code&gt;cleanupMap&lt;/code&gt;, &lt;code&gt;activeWatcher&lt;/code&gt;, the &lt;code&gt;onWatcherCleanup&lt;/code&gt; signature, and the no-active-watcher warning.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vuejs.org/api/reactivity-core" rel="noopener noreferrer"&gt;Vue docs — Reactivity API: Core&lt;/a&gt; — &lt;code&gt;watchEffect&lt;/code&gt; / &lt;code&gt;onWatcherCleanup&lt;/code&gt; signatures and the synchronous-execution constraint.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vuejs.org/guide/essentials/watchers" rel="noopener noreferrer"&gt;Vue docs — Watchers guide&lt;/a&gt; — the stale-request rationale and the &lt;code&gt;onCleanup&lt;/code&gt;-parameter instance-binding note.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vuejs/docs/issues/3222" rel="noopener noreferrer"&gt;vuejs/docs #3222 — onWatcherCleanup in async contexts&lt;/a&gt; — open issue documenting the after-&lt;code&gt;await&lt;/code&gt; footgun.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vueuse.org/core/useFetch/" rel="noopener noreferrer"&gt;VueUse — useFetch&lt;/a&gt; — &lt;code&gt;refetch: true&lt;/code&gt; auto-cancel and the &lt;code&gt;abort&lt;/code&gt; / &lt;code&gt;canAbort&lt;/code&gt; / &lt;code&gt;aborted&lt;/code&gt; surface.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://axios-http.com/docs/cancellation" rel="noopener noreferrer"&gt;Axios — Cancellation&lt;/a&gt; — &lt;code&gt;CancelToken&lt;/code&gt; deprecated in favor of &lt;code&gt;AbortController&lt;/code&gt; (added in v0.22.0, October 2021).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
