Search code examples
javascriptasync-awaitfetches6-promise

can async with fetch poll until a condition is met? (survive rejection)


Using fetch API and async/await, is it possible to continue polling indefinitely, regardless of availability of a URL? I anticipate that a URL might become available eventually, so I want to keep trying until a condition is met. Tried to come up with a minimum viable code sample and I'm not sure I pulled it off:

// this is just a placeholder. It will eventually be a function 
// that evaluates something real. 
// Assume validContinue gets updated elsewhere.      
function shouldContinue() {
    return validContinue;
}
      
async function wonderPoll(someUrl) {

  // just a delay mechanism
  function wait(ms = 1000) {
    return new Promise(resolve => {
      setTimeout(resolve, ms);
    });
  }

  // the actual individual poll
  async function pollingFunction(url) {

    const response = await fetch(url, {
      cache: 'no-store'
    });

    if (response.ok) {
      return response;
    } else {
      Promise.reject(response);
    }

  }

  // allegedly keep polling until condition is met. 
  // But the rejected Promise is breaking out!
  while (shouldContinue()) {
    await wait();
    result = await pollingFunction(someUrl);
  }
  
  // when the fetch hits a rejected state, we never get here!
  console.log('done with the while loop, returning last successful result')

  return result;
}

const sampleUrl = 'https://get.geojs.io/v1/ip/country.json?ip=8.8.8.8';
const sampleUrl2 = 'http://totallybroken_fo_sho';

// swap the URL to test
wonderPoll(sampleUrl)
  .then((result) => {
    console.log('got a result', result)
  })
  .catch((err) => {
    console.log('got an error', err)
  });

I see what's happening (I think). The parent call ultimately executes the polling function, which rejects on the Promise. The condition to continue is still theoretically met, but the rejection breaks out of the While loop and sends to rejection directly up. This propagates all the way up to the catch method of the original/initial Promise. It doesn't even hit any code that would have come after the While loop in the case of resolved Promises.

What I don't know is how to prevent that from happening. I think I don't understand the syntax for intercepting and resolving the promise. When I replace Promise.reject in the response parser with Promise.resolve(response), it still ends up rejecting up to the top.

If the URL I provide is valid, it will continue until the condition is no longer met.


Here's a fiddle: https://jsfiddle.net/gregpettit/qf495bjm/5/

To use the fiddle, the "stop" button simulates the condition being met, and I've provided two different URLs that have to be manually swapped (by passing someUrl or someUrl2) to test.

Expected results:

  • with good URL, continuous polling (will have to dig into network in dev tools) until condition is met (by pressing Stop!) and then the calling function's 'then' can show the result.
  • with bad URL, continuous polling until condition is met, and then calling function's 'catch' shows the error

Actual results:

  • positive test case is OK
  • negative test case goes directly to the catch

Solution

  • Change the code in while loop to try/catch so you can catch the error

    result can hold a value when there's no error, or a reason when there is an error

    Once the loop is stopped, you either return the value, or throw with the reason

    As below

    async function wonderPoll(someUrl) {
    
      // just a delay mechanism
      function wait(ms = 1000) {
        return new Promise(resolve => {
          setTimeout(resolve, ms);
        });
      }
    
      // the actual individual poll
      async function pollingFunction(url) {
    
        const response = await fetch(url, {
          cache: 'no-store'
        });
    
        if (response.ok) {
          return response;
        } else {
          Promise.reject(response);
        }
    
      }
    
      // allegedly keep polling until condition is met. But the rejected Promise is breaking out!
      while (shouldContinue()) {
        try {
          await wait();
          const value = await pollingFunction(someUrl);
          result = {value};
        } catch (reason) {
          result = {reason};
        }
      }
      // when the fetch hits a rejected state, we never get here!
      console.log('done with the while loop, returning last successful result')
      if (result.reason) {
        throw result.reason;
      }
      return result.value;
    }
    

    Running example https://jsfiddle.net/twkbo9pg/

    the example includes status in the result, but that is unnecessary (I borrowed code from my Promise.allSettled polyfill and forgot to remove that property)