Aborting requests

JavaScript provides native API for aborting asyncronous processes like web requests by exposing AbortController and AbortSignal interfaces.

Assume we have a search field on our site. Whenever user types in searh query, We send a request to server /api/v1/search?query=<entered query>. We don’t want send requests on every keystroke so we use debounce function debounde(searchRequest, 300). It allows us to trigger search API only when user pauses typing at least for 300ms.

It’s good solution, but as we know the nature of asyncronous requests does not guarantee us the order of execution. It means that requests sent earlier can be fulfilled later. It could cause the problems to our search functionality if we leave the implementation logic naive.

order of execution for async requests is not guaranteed

As you can see from the image above we’ve sent two requests and they’ve come back in the wrong order. It means that we’ll show wrong search results to the user. To fix this situation we could use something like this:

let lastQuery = null;

const querySearchResults = async (query) => {
  try {
    lastQuery = query;

    const res = await fetch(`/api/v1/search?query=${query}`);
    const resJson = await res.json();

    // checking if the query didn't not changed
    if (lastQuery === query) {
      // all good, the query didn't change
      // while request was in progress
      // display received results to user ...
    } else {
      // user has changed the query,
      // the results are not relevant anymore
      // discarding them, doing nothing
    }
  } catch (err) {
    console.error("Error requesting search results", err);
    // ...
  }
};

querySearchResults("load");
querySearchResults(encodeURIComponent("load balancer"));

But it’s pretty clunky and a better way of handling it would be to abort the previous request when user sends a new one. That’s where AbortController and AbortSignal step in.

AbortController

To start lets create an instance of AbortController.

const controller = new AbortController();
const { signal, abort } = controller;

AbortSignal

The instance exposes signal property and abort() method.

signal allows us to subscribe to abort event:

signal.addEventListener("abort", (event) => {
  console.log(signal.aborted); // true
  console.log(signal.reason); // new query
});

So whenever abort() method is called the above event handler will be triggered. This method allows to pass a reason when called. It will be available in the event handler event as reason property. It can be handy if you want to support and handle different cases of aborting:

abort("new query");

Cancelable asynchronous API

The nice thing is that JavaScript support many cancelable APIs our of the box. It means that we can pass controller’s signal property say into fetch() method and trigger signal.abort() when user chnges search query:

const querySearchResults = (query, signal) => {
  try {
    const res = await fetch(
      `/api/v1/search?query=${query}`,
      { signal }
    )
    // ...
  } catch (err) {
    if (error.name === 'AbortError') {
      console.log('Request has been aborted')
    } else {
      console.error(err)
    }
  }
}

const controller1 = new AbortController()
querySearchResults('load', controller1.signal)

controller1.abort()
const controller2 = new AbortController()
const query = encodeURIComponent('load balancer')
querySearchResults(query, controller2.signal)

// ...

React

Assuming we have Search component in react we could implement somethin like this:

const Search = () => {
  const [query, setQuery] = useState()

  useEffect(() => {
    const controller = new AbortController()
    const { signal } = controller

    querySearchResults(query, signal)

    return () => controller.abort()
  }, [query])

  return <>...<>
}

On top of that you can and should combine it with debounce() to get the best user experience.

Node.js

The same API is available in Node.js. It can be used with timers, fs.readFile, fs.writeFile, https.request etc.