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:
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:
Top comments (1)
Hey @aviskarkc10 ! Better is to place ‘isLoading = false’ into a ‘finally’ block.