JavaScript's Abort Controller

Sun, Mar 23, 2025

Managing unintended multiple API requests in Vue applications can be tricky, but there’s an elegant solution using JavaScript’s built-in AbortController.

At work, we have encountered various instances of multiple API requests happening when only one request was initiated by a user. For example, a user would click a link to a company’s profile page. The main company profile parent component will load which loads its children, some of which make API requests. Due to the component’s structure and code - which admittedly needs refactoring but technical debt is an unfortunate fact of life here - some of the child components will re-render causing multiple requests. We were seeing 2 requests per child component rather than the expected one.

Some devs on the team attempted to address this in various ways. The initial “fixes” were simply setTimeout wrappers for the data fetching methods. And while this technically “worked,” I saw it as an inelegant and lazy solution to the issue. I saw commits that changed the millisecond parameter from 200, to 600 to 300. It was all arbitrary based on the timing of the response in the dev’s machine, environment, internet speed, etc.

Other attempts included the use of a time-based solution. In a nutshell, a timestamp was recorded when the request was initiated and compared to the current time. This did not work in a predictable or reliable manner.

JavaScript’s Abort Controller

I had read about the Abort Controller via MDN’s using fetch guide and thought I’d give this a try. And wow, it’s pretty amazing. JavaScript gives us a near-effortless method to cancel requests that can be used to properly handle multiple successive API requests.

After a little testing within the component, I knew this was the best recourse. However, this needed to be a more generic solution since I knew we would need this in other places in our app. Enter Vue composables.

useFetchWithAbort

While not the best name for a composable, I think it clearly communicates its purpose. Here’s a modified version of the composable that takes a url to fetch as a parameter to the fetchData method (for our purposes at work, I created it so that we can pass in our Pinia store action):

Let’s break down how this composable works and why it’s effective at handling multiple API requests:

First, we set up our reactive references:

const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const controller = ref<AbortController | null>(null);

The important part happens in the fetchData function. Before making a new request, we check if there’s an existing controller and abort any in-flight requests:

if (controller.value) {
  controller.value.abort();
}
controller.value = new AbortController();

This is crucial because it ensures we’re not dealing with race conditions where multiple requests are competing to update our state. Instead of arbitrary timeouts or timestamp comparisons, we’re explicitly telling the browser “hey, cancel that previous request, it is no longer needed.”

The error handling is particularly nice because it differentiates between aborted requests and actual errors:

if (err.name == "AbortError") {
  error.value = `${err}: fetch was cancelled`;
} else {
  error.value = err;
}

I contemplated adding cleanup duties to the composable itself using Vue’s onScopeDispose method. According to the Vue docs:

This method can be used as a non-component-coupled replacement of onUnmounted in reusable composition functions, since each Vue component’s setup() function is also invoked in an effect scope.

However, after some thought - and yes, having implemented it - I decided against it. Code-wise, adding the cleanup to the composable made for a more complex implementation adding additional memory usage for timeout tracking, and some slight overhead. But more importantly, I realized that the cleanup was not the responsibility of the composable - at least from my understanding of Vue’s philosophy. Leaving this responsibility to the component keeps my composable simpler and easier to test, focused on its sole responsibility, while providing a reset method for components to call when needed, e.g. during unmount, giving them control over when the cleanup occurs.

For a simple implementation example, check out this StackBlitz.

Conclusion

This composable has solved our multiple request issues in a clean, predictable way. No more guessing at timeout values or dealing with race conditions. The AbortController approach is:

  1. Deterministic: It works the same way regardless of network conditions or environment
  2. Efficient: We’re not waiting for unnecessary requests to complete
  3. Clean: The implementation is straightforward and the usage is simple
  4. Reusable: We can drop this into any component that needs to handle API calls

In our production environment, this has eliminated the “duplicate request” issues we were seeing in our company profile pages. The best part? We didn’t need to restructure our entire component hierarchy to fix the problem.

While we could have spent time refactoring our component structure (which we should - but that’s another battle), this composable gives us a practical solution that works with our existing codebase.

I think this approach should be a standard use, whether the fetch requests are causing multiple requests or not, to prevent unnecessary requests - and possibly unintended re-renders. I highly recommend giving this approach a try. It’s been a game-changer for us, and the implementation is simple enough that you can adapt it to your specific needs.

And since you’re here, have a listen to Marcha de los Tristes by Lng Sht, ponkero rapero de Cancún. ¡Ajua!



Back to blog