Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to abort a batch of async fetch requests?

I currently have a list of coordinates, which i use to fetch nearby hikes. Lets say I update my list state to new coordinates, but the fetch requests for the previous coordinates were not complete-- How can i cancel all the fetch requests made for the previous coordinates and only call new fetch requests based on the new coordinates?

I tried implementing AbortController but its probably not working as it's not cancelling a "batch" of fetch requests, but only single fetch request. My code is below:

  let abortController = new AbortController();
  const getNearbyHikes = async (coordinates) => {
    abortController.abort();
    abortController = new AbortController();
    for (const coord of coordinates) {
      if (coord) {
        try {
          const { lat, lng } = cord.coordinates
          const response = await fetch(
            'http://localhost:5000/category/' + lat + "/" + lng + '/' + searchCategory,
            {signal: abortController.signal}
          )
          const result = await response.json();
          setHikes((prevState) => [...prevState, ...result.businesses])
        } catch (error) {
          if(error.name === 'AbortError'){
            return;
          }
          throw error
        }

      }
    }
  }

Also , I want to update my UI with the hikes as I get them so it doesnt seem like my web app is really slow so I avoid using Promise.all. Any suggestions or help is greatly appreciated.

like image 764
benwl Avatar asked Dec 18 '25 15:12

benwl


1 Answers

The main problem with the cancellation is that you're providing the signal to fetch incorrectly. You're providing the signal as a second argument, but fetch expects an "init" object there with a signal property on it.

As @Keith points out, though, your current code is making one call at a time. That will work, but you probably would benefit from doing the calls in parallel.

Something like this:

const getOneResult = async ({ lat, lng }, signal) => {
    try {
        const response = await fetch(
            "http://localhost:5000/category/" +
                lat +
                "/" +
                lng +
                "/" +
                searchCategory, // *** Where does this come from?
            { signal }
        );
        if (signal && signal.aborted) {
            throw new DOMException("Request cancelled", "AbortError");
        }
        if (!response.ok) {
            throw new Error(`HTTP error ${response.status}`);
        }
        return response.json();
    } catch (error) {
        if (error.name === "AbortError") {
            return null;
        }
    }
};
const getNearbyHikes = async (coordinates, signal) => {
    controller.abort();
    controller = new AbortController();
    const results = await Promise.all(
        coordinates.map((coord) => getOneResult(coord.coordinates, signal))
    );
    if (signal && signal.aborted) {
        return;
    }
    const businesses = [];
    for (const result of results) {
        businesses.push(...result.businesses);
    }
    setHikes((prevState) => [...prevState, ...businesses]);
};

Finally, I would have the caller supply the signal rather than using a global one for all calls to getHikes:

const getOneResult = async ({ lat, lng }, signal) => {
    try {
        const response = await fetch(
            "http://localhost:5000/category/" +
                lat +
                "/" +
                lng +
                "/" +
                searchCategory, // *** Where does this come from?
            { signal }
        );
        if (signal && signal.aborted) {
            throw new DOMException("Request cancelled", "AbortError");
        }
        if (!response.ok) {
            throw new Error(`HTTP error ${response.status}`);
        }
        return response.json();
    } catch (error) {
        if (error.name === "AbortError") {
            return null;
        }
    }
};
let controller = new AbortController();
const getNearbyHikes = async (coordinates) => {
    controller.abort();
    controller = new AbortController();
    const results = await Promise.all(
        coordinates.map((coord) => getOneResult(coord.coordinates, signal))
    );
    if (signal && signal.aborted) {
        return;
    }
    const businesses = [];
    for (const result of results) {
        businesses.push(...result.businesses);
    }
    setHikes((prevState) => [...prevState, ...businesses]);
};

then the caller controls aborting the request. But the former may be fine for your use case.


Side note: You'll see I've added a check for response.ok in there. Your code was assuming that since the fetch promise was fulfilled, the HTTP call worked, but unfortunately that's a footgun in the fetch API: it only rejects its promise on network failure, not HTTP failure. You have to check for the latter explicitly.

like image 130
T.J. Crowder Avatar answered Dec 20 '25 07:12

T.J. Crowder



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!