Bonus: Axios cancelling promises refactor

Marissa Biesecker
Red Squirrel
4 min readJan 6, 2023

--

Awhile ago, I had tackled a performance bug that when a user would search and filter a data table in the UI, resulted in too many requests being made and a backlog of responses being sent to be rendered. As I was writing about the experience and providing links to the documentation, I discovered that the approach I had taken had been deprecated in a favor of an approach similar to that used by the Fetch API. Having philomathic tendencies, I knew I was going to need to refactor my solution and share that process as well.

I took a very similar approach to the refactoring as I did to the original problem. First, I updated Axios from v0.21.1 to 0.26.1. I could have just updated to v0.22.0, but took a look at the change logs, and for the purposes of this refactor, didn’t see any reason not to try to bump all the way to the latest (which now is even on a major version bump!).

After updating the versions, I wanted to run a similar experiment as I did the first time, so I added my test method and route back into my rails app, and set up my lol.js test file again. I ran the previous version of the code, just to check, and it was still working as expected.

I was excited. It should be easy to refactor, I thought, since another great example was given in the documentation, I could easily use that and modify it to reflect the previous example, like so:

const controller = new AbortController();axios.get('http://localhost:3000/v0/lol?wat=1', {
signal: controller.signal
}).then(function(response) {
console.log(response.data)
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
console.log('failed 1', thrown)
}
});
// cancel the request
controller.abort()

Well, in my world of programming, things rarely ever go so smoothly. The bugs like me, and I like the bugs. They give me an opportunity to learn and grow. When I tried to execute node lol.js with the new method above, I got:

const controller = new AbortController();
^
ReferenceError: AbortController is not defined

as a result in my terminal. How frustrating when the expectation was that it was going to be a simple update. Well, time for some sanity checks and exploration.

The first check I did was to check the node modules to make sure that Axios did indeed get updated, and re-run yarn add . I’ve had some experiences with funky installations in the past, and wanted to make sure that wasn’t the problem. The node modules folder seemed to check out, so next, I looked a bit closer at the Axios docs again. Very fortunately, one of the latest releases mentioned a fix related to Node.js that triggered in my mind to check the version of node I was running.

Since I like to work on multiple projects at once, I typically use a node package manager; my current setup uses Nodenv, but I’ve also used NVM. My system node was still running on v14.17.3, which is pretty old. The recommended version is v16.4.2. On another project I was working on, I needed v16.5.0, so to test my theory, I ran the command to change my local node environment to v16.5.0.

And voila! That’s what it was. The response I received in my terminal was `Request canceled canceled`. Now, I wanted to add the second request, like I had done before, to check how and when the responses are getting canceled. With the previous version, it was easy to reason and hypothesize in the example that the first request would be canceled, but the second would result in a response, because a token was created and associated with each response, which the cancel method would be called on.

With this new AbortController, I had another hypothesis. I wanted to test if I created just one AbortController instance, if it would cancel both requests. This didn’t seem possible with the cancel method, or perhaps it was, if tokens were shared across requests. Then, if it would cancel both, if 2 AbortControllers are created, if I just cancel the first request, the second will result in a response, and will match the results of the first experiment with the cancelTokens.

These assumptions and hypothesis turned out to be correct! With one AbortController, both requests were canceled.

Request 1 canceled canceled
Request 2 canceled canceled

I also discovered that I if I moved the call to abort before the second response was made, it would cancel that request first:

Request 2 canceled canceled
Request 1 canceled canceled

Not really useful for the current use case, but it did help solidify the need to create a new AbortController per response, otherwise the last request, which we want to render would also be canceled! So, what were the differences at the end of the day?

const CancelToken = axios.CancelToken;
const source1 = CancelToken.source();
const source2 = CancelToken.source();

changed to:

const controller = new AbortController();
const controller2 = new AbortController();

which meant passing new variables to the request, instead of cancelToken: source1.token to

axios.get('http://localhost:3000/v0/lol?wat=1', {
signal: controller.signal
})

And finally, the method call to cancel or abort has also changed from source1.cancel(‘Operation canceled by the user.’) to controller.abort() .

Fortunately, very little needs to change. This holds true in the actual code implementation as well. The error handling didn’t need to change. The biggest change was renaming cancelToken to signal and updating the hooks request to use the new AbortController and method.

Check with green tests, open for review and finally:

Relatively smooth and ultimately satisfying!

--

--