Cancelling Requests with Abortable Fetch

There are often times in a web application where you need to send a request for the latest user input or interaction. Some examples might be a autocomplete or zooming in and out a map. Let's think about each of these examples for a moment. Firstly autocomplete; every time we type (or maybe less if we were to debounce) we might send out a request. If the user input changes the old requests might become irrelevant as we keep typing (i.e. 'java' and 'javascript'). That's potentially a lot of redundant requests before we get to what we're interested in!

Now the web map case; we're zooming and panning around the map. As we zoom in and out, we are no longer interested in the tiles from the previous zoom levels. Again, lots of requests might be pending for redundant data.

Taking the first example, let's set the scene by looking at some naive code about how we might implement an autocomplete. For the purpose of this article we will be using the more modern fetch rather than XMLHttpRequest for making a network request. Here's the code:

autocompleteInput.addEventListener("keydown", function () {
const url = "https://api.example.com/autocomplete";

fetch(url)
.then((response) => {
// Do something with the response
updateAutocompleteMenu();
})
.catch((error) => {
// Something went wrong
handleAutocompleteError(error);
});
});

The problem in this case is that each one of these requests will complete, even if it is no longer relevant. We could implement some extra logic in the updateAutocompeleteMenu to prevent unnecessary code execution but this won't actually stop the request. It's also worth noting here that browsers have a limit of outgoing requests which means that they queue requests once that limit is hit (although that limit varies by browser).

Abortable Fetch #

A new browser technology that we can leverage to solve the aforementioned issue is Abortable Fetch. Abortable fetch relies on a browser specification for AbortController. The controller has a property called signal which we can pass to our fetch as an option (also named signal), and then use this at our later convenience to cancel the request with the controllers abort method.

An example might look a little like this:


const url = "https://api.example.com/autocomplete"
let controller;
let signal;

autocompleteInput.addEventListener('keyup', () => {

if (controller !== undefined) {
// Cancel the previous request
controller.abort();
}

// Feature detect
if ("AbortController" in window) {
controller = new AbortController;
signal = controller.signal;
}

// Pass the signal to the fetch request
fetch(url, {signal})
.then((response) => {
// Do something with the response
updateAutocompleteMenu()
})
.catch((error) => {
// Something went wrong
handleAutocompleteError(error);
})
});

});

Here we do feature detection to determine if we can use AbortController (it's supported in Edge, Firefox, Opera and coming in Chrome 66!). We also determine if a controller has already been created, and if so we call controller.abort() which will cancel the previous request. You can also use the same signal in multiple fetches to cancel multiple fetches at once.

A little demo #

I've created a small demo showing how to use Abortable Fetch, loosely based on the idea of the autocomplete idea (without any of the implementation details!). What happens is every time you type it makes a network request. If you make a new keystroke before the old request has completed it will abort the previous fetch. It looks a little something like this in practice:

You can check the code out here.

Thinking beyond fetch #

Perhaps the coolest part about AbortController is it has been designed to be a generic mechanism for aborting asynchronous tasks. It is part of the WHATWG specification, meaning it is DOM specification rather than a language (ECMAScript) specification, but for frontend development this is still a useful feature. You could leverage it as a cleaner async control flow mechanism for times you implement asynchronous tasks (i.e. when using Promises). Feel free to take a look at Bram Van Damme super article for a more detailed example of what I'm talking about.

Published