Canceling JavaScript Requests

Marissa Biesecker
Red Squirrel
6 min readJan 6, 2023

--

For anyone working to bridge the gap between the backend, or server side, of a web application and the frontend, or browser side, of a web application knows there are two modern popular options for interactions with the request-response protocol, better known as http(s)¹. Those main options are the Fetch API², which is the new built-in JavaScript method built on promises, and Axios API, an optional add-on library which is also promise based.

There is a lot of comparing and understanding to be done between the two when starting a new project to determine which might fit a project’s needs better, but that will not be the focus here. The project that I was working on already included and utilized Axios, so while I could choose either library for I wanted to stick with Axios.

I was building a frontend interface for business stakeholders to query our database and view the results in thoughtfully designed tables. Due to the amount and complexity of the queries, these requests for data could take multiple seconds to run. Additionally, there were multiple ways to filter and search for the information needed, creating the possibility of firing off multiple requests, as a new request was sent each time the user used a filter or search term to build one. So the initial need to send requests and receive responses was easy to solve. However, with this very basic implementation and the possibility of using multiple filters, which created multiple requests, was creating a lag in displaying the most current requested information. It was also showing simpler requests that took less time to resolve out of order because more complex queries took longer to resolve, all making for a confusing and undesirable user experience.

The problem has now become too many requests being made and a backlog of responses being sent to the UI that don’t need to be displayed. The need has narrowed to become a way to limit requests or only display the very last request, when the user has finished their searching and filtering selections for advanced querying. Ok, so how to implement?

One of the first possibilities that comes to mind is utilizing event listeners in the UI components to wait until the user has finished selecting filters and searching. Upon further reflection, this is a pretty naive idea and to implement would be needlessly complex. There are multiple UI components that would need to be listened to, and trying to guess which to listen to, and how long to wait afterwards seems impossible. It also still wouldn’t solve the problem of long running, complex queries still needing to return and render before a new selection is made. Another, more reasonable possibility is to stop the previous query from completing its response and only returning the current query. So, is this possible and how?

Turns out it is possible. By canceling the requests. Because it is built with Javascript promises, it is possible to cancel the requests with both Fetch and Axios. This problem was fun to solve because there aren’t a ton of articles out there on it, unlike other popular topics. It was also well scoped and the great Axios documentation led to quick testing and implementation. So, if you are interested in how to cancel a Fetch request, check out this article, otherwise if you’re interested in how to cancel an Axios request and how I did it, keep reading!

A quick disclaimer: I originally encountered this problem and started the outline for this some time ago, and was running on Axios v.0.21.1. I see that since then, in an ensuing minor version, the implementation has changed, which means time for a bonus refactor in another article!

But first, I’ll run through the approach for the original solution. As stated previously, I wasn’t finding too many articles or step by step approaches to implementing the cancelation of an Axios request, however, the team there has excellent documentation. So, I made a quick little experiment to test out canceling my own request, which I will get to after a bit of extra background.

For additional background on the project, it was a Ruby on Rails legacy project that was slowly being turned into just an API with a React frontend. Thus for easy proof of concept testing, I first wanted to try hitting the Rails API, and then figure out how to plug it into our custom React app.

Disclaimer number 2: I really love learning new things by pairing with other people. It’s so much more fun to figure something out with a pair, especially because when learning something new, it’s really easy to get overwhelmed and make mistakes or miss a simple step, which is much less likely to happen with another set of eyes! So, at some point of this problem solving process, “I” became “we”, for a bit before I went solo again, and I would highly recommend tackling this or anything else new with a buddy as well!

It went like this — in our Rails app:

  1. Add a view to the controller. We know that we want to test multiple requests that take different times to resolve to see if we can cancel one, or both. Then we hypothesize which will be rendered and test which result is actually rendered. We added a timer method to simulate responses taking extra time to compute.
def lol
if params[:wat] == '1'
sleep 1
render json: '"one"'
else
render json: '"two"'
end
end

2. Add a route. We need to be able to hit our API endpoint and get the responses.

get '/lol', to: "view#lol"

3. Then we added the test which would simulate the request that would be made on the frontend. Just like our path naming, which make our experiments a little more fun, we called this little test program lol.js. We can then call this program from the root of our project with node, node lol.js, and see the logged responses.

const axios = require('axios');
const CancelToken = axios.CancelToken;
const source1 = CancelToken.source();
const source2 = CancelToken.source();
axios.get('http://localhost:3001/v0/lol?wat=1', {
// set the token
cancelToken: source1.token
}).then(response => {
// log response
console.log('response 1', response.data)
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
// log request if canceled
console.log('Request 1 canceled', thrown.message);
} else {
// handle error
console.log('failed 1', thrown)
}
});
// cancel the request with a message
source1.cancel('Operation canceled by the user.');
axios.get('http://localhost:3001/v0/lol?wat=2', {
cancelToken: source2.token
}).then(response => {
console.log('response 2', response.data)
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request 2 canceled', thrown.message);
} else {
console.log('failed 2', thrown)
}
});

This test was really just “copy-pasta” from the documentation. It’s a good sanity check to ensure that the documentation code works as expected before trying to make changes to it, so you know if anything goes wrong, it’s not because a bunch of things changed.

With the test is working as desired, we had our path and I needed to start looking at the actual implementation in the React code. The real implementation is never quite as simple! The existing code wrapped the API calls in a custom usePromise hook, which needed to be updated to cancel the request. So I added a catch to our call back request function, with a short comment for clarification.

.catch(error => {
if (axios.isCancel(error)) {
// Cancel request without dispatching an actual error.
return;
}
}

The custom request function is also contained in a custom wrapper, which needs to pass the token too.

export default async function request(
path,
method,
{ body, headers, baseUrl, params, withCredentials = true, cancelToken } = {}
) { .... }

Then the `cancelToken` needs to be set in state to make sure the previous request is canceled and generates a new token when a new request is made.

const [cancelToken, setCancelToken] = useState();
const { data, isLoading, error, request } = usePromise(() => {
// For every new request made, the previous request is canceled.
cancelToken?.cancel('Newer request made.');
const tokenSource = axios.CancelToken.source(); setCancelToken(tokenSource); return getRequest(filter, page, 20, tokenSource.token);}, {
effectCondition: [page, filter],
isEffect: true,
});

And voila! Now every request that is made has a token, and when a new request is made, that request is canceled and a new token is created for the new request. The server is now saved from being bombarded and backlogged with requests that are no longer relevant and the UI displays much more smoothly and doesn’t suddenly change because a backlogged request is finally resolved!

Bonus: refactor!

  1. The ‘s’ stands for secure. Secure because the data sent back and forth on the connection is encrypted for https and is only plain text via http.
  2. Fetch being the new API standard built off of XMLHttpRequest, which Axios is also using under the hood.

--

--