DEV Community

Aviskar KC
Aviskar KC

Posted on

Using AbortController out in the wild

I recently had to cancel requests made by fetch in one the projects I was working on and had a chance to use AbortController. Now there are some really good resources to learn about AbortController like this one by Jake Archibald, but very few showcase using it in a real life scenario. Usually the examples in these tutorials will have a button to make an api call and a second one to cancel it. Although that's a good example to get started with, but I can't think of a case where I might ever build some thing like that in a "real" project. So here's an example where you might actually use AbortController in a real life scenario.

Imagine a search bar, where you need to make the api call to fetch data as you type. Something like this:

Alt Text

Now, you will definitely come across a situation where a promise resolves faster than a previous one and you will be left displaying stale data to the user. You can definitely use ol' reliable debounce for this, but that still doesn't solve your issue all the time.

This is where AbortController comes to your rescue!!! If a new api call is made while the previous one hasn't resolved, you can cancel the previous one using AbortController.

If you wanna jump into the code straight away here's a demo, but if you wanna know more what's going on, you can follow the blog furthermore:

In the index.html file, we have our input field.

 <input
   class="search-field"
   type="text"
   id="search"
   placeholder="Search for a joke"
>

On every keyup event, this input field fires a call to fetch data from our api:

// variable to track whether or not we are fetching data
let isLoading = false;

// event listener for our input field 
searchEl.addEventListener("keyup", e => {
  fetchQuote(e.target.value);
});

// function to call api and fetch data based on our search query
async function fetchQuote(search) {
  try {
    isLoading = true;

    const response = await fetch(
      `https://api.chucknorris.io/jokes/search?query=${search}`,
      { signal }
    );

    const data = await response.json();

    const jokes = data.result.slice(0, 5);

    isLoading = false;
    renderJokes(jokes);
  } catch (err) {
    isLoading = false;
  }
}

Note that we have a isLoading variable to tell us whether or not we have a pending promise.

Now that the logic for calling our api is done, let's initialize our AbortController:

let abortController = new AbortController();
let signal = abortController.signal;

And now to actually cancel our api call inside the fetchQuote function you can add abortController.abort() function:

async function fetchQuote(search) {
  try {
    // Cancel our api call if isLoading is true
    if (isLoading) {
      abortController.abort();
    }

    isLoading = true;

    // Pass the "signal" as a param to fetch
    const response = await fetch(
      `https://api.chucknorris.io/jokes/search?query=${search}`,
      { signal }
    );
    // rest of the function is same as above
  } catch(err) {
    isLoading = false;
  }
}

Now that aborted request is cancelled, it actually goes to our catch block. Since technically this isn't an error, we can bypass this by checking for abort errors:

catch(err) {
  // Only handle errors if it is not an "AbortError"
  if (err.name === 'AbortError') {
    console.log('Fetch aborted');
  } else {
    console.error('Uh oh, an error!', err);
  }
}

Now any request we make cancels the previous request if it hasn't resolved yet.

But there is a catch, this doesn't work for subsequent requests and only works for the first request. For AbortController to work for all of our subsequent requests, we need to create a new one each time we abort a request. Which leaves us with the following:

async function fetchQuote(search) {
  try {
    if (isLoading) {
      // Cancel the request
      abortController.abort();

      // Create a new instance of abortController
      abortController = new AbortController();
      signal = abortController.signal;
    }

    isLoading = true;

    const response = await fetch(
      `https://api.chucknorris.io/jokes/search?query=${search}`,
      { signal }
    );

    const data = await response.json();

    const jokes = data.result.slice(0, 5);

    isLoading = false;
    renderJokes(jokes);
  } catch (err) {
    isLoading = false;
    if (err.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Uh oh, an error!', err);
    }
  }
}

And now we are finally able to successfully use abort-able fetch requests out in the wild:

Alt Text

Top comments (1)

Collapse
 
alepop profile image
Aleksey

Hey @aviskarkc10 ! Better is to place ‘isLoading = false’ into a ‘finally’ block.