Canceling requests in Javascript
Tags: javascript, programming, typescript
Published: 2023-12-20 07:00:05

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 resource associated to them get released as well
- It prevents unnecessary data transfer and the subsequent parsing of the response, which can be a heavy task depending on the size of the response
- Prevents unexpected but common issues such as accidentally overwriting newer information when a stale request gets resolved
- Simplifies the code as you no longer need to have custom logic to ignore stale requests
- It opens the possibility to free resources in the backend as well if you listen to when clients cancel their requests

## The API

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

```js
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 `.signal`property as a property to the fetch function:

```js
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:**

- Reusing an `AbortController` for a new fetch request will cancel the request immediately if the signal was aborted in the past
- Aborting a signal after a request has been resolved **won't trigger any error**

### Cancelling multiple fetch requests

You can use one signal to cancel multiple fetch requests:

```js
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. 

```js
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`**.

```js
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](https://axios-http.com/docs/cancellation).

### 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](https://tanstack.com/query/v4/docs/react/guides/query-cancellation).

```ts
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:

```js
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](https://caniuse.com/abortcontroller), and It could improve a lot the experience of the people using our products, as well as our experience as developers.