Back

Forever Functional: Three ways of Polling

Forever Functional: Three ways of Polling

In a previous article of this series, we showed how to implement waiting with promises — we wanted to efficiently observe some condition and react when the condition became true. In this case, we’re doing something a bit similar, but different: we want to do polling of an API, an essentially asynchronous task, and eventually act if needed.

For more detail, this is the situation. We want to poll some API and check the results. If the results satisfy some condition, we want to do some special work. For instance, we could be waiting for some data to be available; when that happens, we would want to do something (refresh the screen? do some extra work?) and stop polling.

In our article on waiting, we based all the solutions on promises. Here, though we’ll also have a promise-based solution, we’ll also use other techniques and features for variety and to broaden our set of tools. Namely, we will see study:

  • setting up a loop to periodically call the API and do the tests
  • setting up a function to be called at regular intervals to do polling
  • using a promise to do the calling and checking for us.

We’ll get to the code immediately, but first let’s see some extra functions we’ll be using.

Getting ready

To develop our examples, we’ll need some fake promises (we don’t want to use an actual one) so we’ll re-use code from previous articles.

const success = (time, value) =>
  new Promise((resolve) => setTimeout(resolve, time, value));

const failure = (time, reason) =>
  new Promise((_, reject) => setTimeout(reject, time, reason));

The first function returns a promise that will resolve to a value after a given time, and the second function returns a promise that will reject (with a given reason) after some time. All times must be given in milliseconds, as per JavaScript’s standard.

We’ll also want to let time pass, so we’ll also use the following:

const timeout = (time) =>
  new Promise((resolve) => setTimeout(resolve, time));

This creates a promise that resolves itself after some time. To delay for three seconds, we’d then write:

await timeout(3000);

In the rest of the article, we’ll fake calling an API with something like the following, which waits for 0.4 seconds and then provides the time and an "all ok" result.

const callFakeApi = () => {
  console.log(new Date(), "Calling API");
  return success(400, "all ok");
};

To test the results, we’ll use functions such as the following — it simulates that the condition failed a few times, and then it succeeds. We want to have some variety; if polling succeeded at the first try, we wouldn’t be able to truly test our logic.

let count = 0;

const testCondition = () => {
  count++;
  console.log(new Date(), "Testing", count, count === 4 ? "OK" : "Not yet...");
  return count === 4;
};

The work we’ll do is absolutely nothing — whenever our simulated API call succeeds, we’ll log the time and some text.

const doSomething = () => console.log(new Date(), "Doing something...");

We are now set; let’s start considering solutions to our polling problem. We want to have some way to start polling and also a way to stop it in case some condition in our app determines we should not continue with that. We’ll see three distinct ways of coding this; it’s not that we need so many versions, but that we’ll use different techniques and JavaScript functionalities, so it will be a learning experience.

A first solution, with a loop

Let’s go with the simplest solution: we do a loop in which we wait some time, call the API, test the condition, and keep looping until the condition is satisfied or polling is canceled.

Our function will receive four parameters — and these will be the same for the other polling implementations we’ll be seeing:

  • callApiFn is a function that will call the external API
  • testFn will test whatever the API returned; if this test is satisfied, it means we must do something
  • doFn is the function that will be called upon a satisfactory testFn result
  • time is the delay between polling calls.

Let’s now see the actual code for our startPolling function:

function startPolling(callApiFn, testFn, doFn, time) {
  let polling = true;                                         // [1]

  (async function doPolling() {
    while (polling) {                                         // [2]
      try {
        let result;
        if (polling) {                                        // [3] 
          await timeout(time);
        }
        if (polling) {                                        // [4]
          let result = await callApiFn();
        }
        if (polling && testFn(result)) {                      // [5]
          stopPolling();
          doFn(result);
        }
      } catch (e) {                                           // [6]
        stopPolling();
        throw new Error("Polling cancelled due to API error");
      }
    }
  })();                                                       // [7]

  function stopPolling() {                                    // [8]
    if (polling) {
      console.log(new Date(), "Stopping polling...");
      polling = false;
    } else {
      console.log(new Date(), "Polling was already stopped...");
    }
  }

  return stopPolling;                                         // [9]
}

Let’s go over the code.

  1. we define a polling variable; if it becomes false, we stop polling.
  2. we set up a loop that will continue until polling is successful or aborted.
  3. why are we testing polling? Remember we are running async code; despite the while at (2), now polling could be canceled. If polling continues, we take some time before actually calling the API.
  4. We call the API if we’re still working (see the previous point) after time has elapsed.
  5. again, if polling wasn’t canceled, we test the API results; if they were satisfactory, do stop polling and do whatever extra was expected.
  6. if there was some error, we stop polling and throw an exception
  7. the doPolling function is actually a IIFE; we define the function —giving it a name just for clarity— and immediately call it
  8. the stopPolling function will allow us to cancel polling if it is running; setting polling to false stops things.
  9. the result of a call to startPolling() is the function we need to cancel polling, should we want to.

How would we use this? An example follows:

console.log(new Date(), "Starting polling");
const stopPolling = startPolling(callFakeApi, testCondition, doSomething, 1000);
await timeout(6300);
console.log(new Date(), "Canceling polling");
stopPolling();

Running this produces:

2023-03-20T03:04:50.530Z Starting polling
2023-03-20T03:04:51.540Z Calling API
2023-03-20T03:04:51.741Z Testing 1 Not yet...
2023-03-20T03:04:52.744Z Calling API
2023-03-20T03:04:52.945Z Testing 2 Not yet...
2023-03-20T03:04:53.946Z Calling API
2023-03-20T03:04:54.147Z Testing 3 Not yet...
2023-03-20T03:04:55.149Z Calling API
2023-03-20T03:04:55.350Z Testing 4 OK
2023-03-20T03:04:55.351Z Stopping polling...
2023-03-20T03:04:55.351Z Doing something...
2023-03-20T03:04:56.839Z Canceling polling
2023-03-20T03:04:56.840Z Polling was already stopped...

Let’s go into this output in detail; the other implementations in the article will produce similar results.

  • we start polling every 1 second (1000 milliseconds)
  • one second after the start, the API is called for the first time
  • after a (fake) delay, results come in, but the test fails, so we keep polling
  • twice more, we call the API, but the test fails
  • the fourth time the test succeeds; we “do something” and polling stops
  • when we try to cancel polling (6.3 seconds after having started it) we find that polling had already stopped

So, as we see, the code works well. (This solution is akin to the 2nd solution, “sleep and loop”, we saw in the “Waiting with Promises” article.) If we start this polling loop, it will keep working asynchronically (so you can do something else) but should we get an API answer that passes some check, a specific action will be taken. Of course, nothing forbids starting polling again; this is up to you. The key aspect is that our logic works well — let’s now consider alternative versions.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

A second solution, with intervals

The solution we just saw works well, but there’s a more obvious way of working — to repeatedly do something at intervals, we should use JavaScript’s own setInterval() function.

We’ll write a new startPolling() function with the same parameters and specs as in the previous section.

import {
  timeout,
  callFakeApi,
  testCondition,
  doSomething,
} from "./pollingCommon.mjs";

function startPolling(callApiFn, testFn, doFn, time) {
  let intervalId = setInterval(() => {                        // [1]
    callApiFn()                                               // [2]
      .then((data) => {
        if (intervalId && testFn(data)) {                     // [3]
          stopPolling();
          doFn(data);
        }
      })
      .catch((e) => {                                         // [4]
        stopPolling();
        throw new Error("Polling cancelled due to API error");
      });
  }, time);

  function stopPolling() {                                    // [5]
    if (intervalId) {
      console.log(new Date(), "Stopping polling...");
      clearInterval(intervalId);
      intervalId = null;
    } else {
      console.log(new Date(), "Polling was already stopped...");
    }
  }

  return stopPolling;                                         // [6]
}

How does this work? Let’s see the details.

  1. we use setInterval() to create an eternal loop that periodically calls the API, checks its results, etc. When polling is canceled, intervalId will be null.
  2. we call the API
  3. when results arrive, if polling wasn’t canceled, we check the results, and if the test passes, we stop polling and do something special
  4. on any API error, we stop polling and throw an exception
  5. to stop polling (if it was running) we use clearInterval() and set intervalId to null, as we mentioned above regarding item (1)
  6. as with the loop-based polling solution, we return the function that can be used to stop polling.

If we run this code, we’ll be getting precisely the same output as in the previous section, to wit:

2023-03-20T03:17:09.981Z Starting polling
2023-03-20T03:17:10.990Z Calling API
2023-03-20T03:17:11.192Z Testing 1 Not yet...
2023-03-20T03:17:11.992Z Calling API
2023-03-20T03:17:12.193Z Testing 2 Not yet...
2023-03-20T03:17:12.993Z Calling API
2023-03-20T03:17:13.194Z Testing 3 Not yet...
2023-03-20T03:17:13.993Z Calling API
2023-03-20T03:17:14.194Z Testing 4 OK
2023-03-20T03:17:14.195Z Stopping polling...
2023-03-20T03:17:14.196Z Doing something...
2023-03-20T03:17:16.290Z Canceling polling
2023-03-20T03:17:16.290Z Polling was already stopped...

So, the implementation is different (in this case, the code is similar to the 3rd solution we saw in the “Waiting with Promises” article) but the performance and results are the same. Let’s consider a third solution, somehow different, based on promises.

A third solution, with promises

Let’s now work out a third approach. We could set up a promise that will be resolved when polling works and the condition is satisfied. (If the API calls fail or polling is canceled, the promise will be rejected.) We can use JavaScript’s setTimeout() method to provide some delay between polling attempts, as follows:

function startPolling(callApiFn, testFn, time) {
  let polling = false;                                        // [1]
  let rejectThis = null;

  const stopPolling = () => {                                 // [2]
    if (polling) {
      console.log(new Date(), "Polling was already stopped...");
    } else {
      console.log(new Date(), "Stopping polling...");         // [3]
      polling = false;
      rejectThis(new Error("Polling cancelled"));
    }
  };

  const promise = new Promise((resolve, reject) => {          
    polling = true;                                           // [4]
    rejectThis = reject;                                      // [5]

    const executePoll = async () => {                         // [6]
      try {
        const result = await callApiFn();                     // [7]
        if (polling && testFn(result)) {                      // [8]
          polling = false;
          resolve(result);
        } else {                                              // [9]
          setTimeout(executePoll, time);
        }
      } catch (error) {                                       // [10]
        polling = false;
        reject(new Error("Polling cancelled due to API error"));
      }
    };

    setTimeout(executePoll, time);                            // [11]
  });

  return { promise, stopPolling };                            // [12]
}

The code is different because here we will return a promise to do polling. Let’s first see how we’ll use the function, to clarify things.

console.log(new Date(), "Starting polling");
const { promise, stopPolling } = startPolling(callFakeApi, testCondition, 1000);
promise.then(doSomething).catch(() => { /* do something on error */ });
await timeout(6300);
console.log(new Date(), "Canceling polling");
stopPolling();

When we call startPolling() we get a promise (to do the polling) and a stopPolling() function (to cancel polling at any moment). The promise will start running, and if the API call succeeds and testCondition() returns true, the then() method will run, and here’s where we’ll do whatever is needed. (On any error, the catch() method will be called, as usual.) The result of this code is, once again, as in the two previous examples, so let’s not waste space; we’ll study the code instead.

  1. We won’t pass the doFn() function because that will be called in the then() promise method. We’ll also have a polling variable (it will be true while polling goes on) and a rejectThis variable to store the reject function for the promise; see (3) and (4) below
  2. the stopPolling() function will test if polling is running, and if so, it will stop it
  3. to stop polling, we reset polling to false, and we use the saved rejectThis function to force the promise to reject immediately
  4. we create a new promise; it starts by setting polling to true to show that polling is running
  5. to be able to force the promise to reject in stopPolling, we have to store the reject parameter in rejectThis; see (3) above
  6. the executePoll will do the polling and testing
  7. we call the API
  8. if polling wasn’t canceled, we test the API results, and if the test passes, we stop polling and resolve the promise
  9. if the test didn’t pass, we set a new poll call after a time delay
  10. on any error, we stop polling and reject the promise
  11. we start polling by setting executePoll to run for the first time after a time delay
  12. this function returns two values: the promise that does the polling, and the function to stop polling

This third implementation is a tad different (and somewhat similar to the fifth solution in the “Waiting with Promises” article, but here we used setTimeout() instead of setInterval()) but we succeeded in showing yet a third way to implement the polling we desired.

Summing up

We saw three different ways of implementing a polling solution: loops, intervals, and promises. Even if we don’t need all the different solutions (anyone will do!) we got the chance to study three alternative implementations and also to compare the results with previous ones from the “Waiting with Promises” article; this kind of comparative study is usually quite beneficial, so review that article as well!

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay