Forever Functional: Three ways of Polling
Tags: functional, javascript, webdev
Published: 2023-03-23 00:53:24

by [Federico Kereki](https://blog.openreplay.com/authors/federico-kereki)

In a previous article of this series, we showed how to implement [waiting with promises](https://blog.openreplay.com/forever-functional-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.

```javascript
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:

```javascript
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:

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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:


```javascript
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](https://developer.mozilla.org/en-US/docs/Glossary/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:

```javascript
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.


<h2>Session Replay for Developers</h2> 
<p><em>
Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data
</em></p>
<img alt="OpenReplay" width="768" height="400" src="https://blog.openreplay.com/assets/overview_Z17Qdyg.png" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em>







## 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()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) function. 

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

```javascript
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()`](https://developer.mozilla.org/en-US/docs/Web/API/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()](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) method to provide some delay between polling attempts, as follows:

```javascript
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.

```javascript
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"](https://blog.openreplay.com/forever-functional-waiting-with-promises/) article; this kind of comparative study is usually quite beneficial, so review that article as well!


[![newsletter](https://blog.openreplay.com/newsletter_Z1P0UIP.png)](https://newsletter.openreplay.com/)