Skip to content

Canceling requests in Javascript

Posted on:December 20, 2023 at 07:00 AM

Canceling requests in Javascript If you’re a developer like me, you probably had to write some custom logic to handle stale requests that you triggered in the past, but whose response you no longer need. But did you know that it’s possible to cancel HTTP requests from your code even after triggering them?

There’s an API for this specific case and I recently had the opportunity to use it, and let me tell you that it’s more useful than what I initially thought. Here are the reasons why you should use it:

Why you should cancel stale HTTP requests?

Cancelling HTTP requests when no longer needed has several advantages and can reduce the complexity of your code by a lot. Some of the benefits you get when you cancel a request instead of just ignoring them are:

The API

All the knowledge I have about Javascript tells me that the JS way to cancel a fetch request would be something like:

const promise = fetch('https://dummyjson.com/products').then(res => res.json()).then(console.log);

// On user activity...
promise.abort() // Or .cancel()

But I was wrong, and yet, the real cancelation API is easy and simple. It basically consists of one object called AbortController that has one method .abort(). Although it’s weird looking for a JS developer, the API is quite straightforward, and it’s also built generic enough to be easily adopted by other APIs.

Canceling a fetch request

Canceling a fetch request is a simple as creating an AbortController instance and passing its .signalproperty as a property to the fetch function:

const controller = new AbortController();

// URL with big file so it takes time to download, taken from mdm web docs
fetch('https://mdn.github.io/dom-examples/abort-api/sintel.mp4', { signal: controller.signal })
  .then(res => {
    console.log("Download completed")
   })
   .catch((err) => {
     console.error(`Download error: ${err.message}`)
   })

const onCancel = () => {
  controller.abort()  
}

Things to note:

Cancelling multiple fetch requests

You can use one signal to cancel multiple fetch requests:

const controller = new AbortController();

fetch('https://dummyjson.com/products', { signal: controller.signal })
fetch('https://dummyjson.com/products', { signal: controller.signal })
fetch('https://dummyjson.com/products', { signal: controller.signal })
fetch('https://dummyjson.com/products', { signal: controller.signal })

setTimeout(() => {
  controller.abort()
}, 400);

Handling user cancelation errors

One thing to notice is that canceling requests will throw an AbortError so you need to handle them in your catch method. In most of the cases, you just want to ignore them, since they are not really errors.

fetch('https://mdn.github.io/dom-examples/abort-api/sintel.mp4', { signal: controller.signal })
  .then(res => console.log("Download completed"))
   .catch((err) => {
     if (err.name !== 'AbortError') {
       console.error(`Download error: ${err.message}`)
     }
   })

Knowing this will prevent you from getting spammed by your logging system —If you have one—.

What about other libraries?

Axios

Since v0.22.0, Axios supports canceling requests out of the box with a similar API. The only thing to watch out for is that Axios returns a different error for canceled requests: CanceledError.

const controller = new AbortController();

axios.get('https://dummyjson.com/products', { signal: controller.signal })
     .catch((e) => {
        if (e.name !== 'CanceledError') {
          console.error(e);
        }
      });

More details here.

React query

Another popular library that supports request cancelation out of the box is React Query.

The following is an example taken from the official documentation.

const query = useQuery({
  queryKey: ['todos'],
  // Get the signal from the queryFn function
  queryFn: async ({ signal }) => {
    const todosResponse = await fetch('/todos', {
      // Pass the signal to one fetch
      signal,
    })
    const todos = await todosResponse.json()

    const todoDetails = todos.map(async ({ details }) => {
      const response = await fetch(details, {
        // Or pass it to several
        signal,
      })
      return response.json()
    })

    return Promise.all(todoDetails)
  },
})

Food for thought

As said before, the entire cancelation API is quite generic, and it’s designed to be implemented by other APIs and be adapted to other usages. The implementations are not only limited to HTTP requests, but could also be used to cancel heavy Javascript logic. For example, the following code could be a costly function that is run somewhere along your entire logic and could use a signal to prevent using resources when not necessary:

const  veryHeavyOperation = (signal) => {
  Array.from({ length: 10 }).forEach((_, index) => {
    if(signal.aborted) return; // If the signal is cancel, don't do anything 🦥
      setTimeout(() => {
        // HEAVY OPERATIONS
        console.log("Heavy Operations: ", index)
       }, 1000);
     });
};

Conclusion

I think this is an underrated API that deserves more credit. It’s easy to use, It’s supported in all the major browsers, and It could improve a lot the experience of the people using our products, as well as our experience as developers.