Think Twice Before Using setInterval() for API Polling – It Might Not Be Ideal
Tags: webdev, javascript, api, programming
Published: 2024-05-19 07:40:01

## Before We Start

1. Data fetching is a very simple task until it becomes complicated. Therefore I recommend that you use powerful data fetching libraries like [TanStack Query](https://tanstack.com/query/latest), [SWR](https://swr.vercel.app/), [RTK Query](https://redux-toolkit.js.org/rtk-query/overview).
2. This post assumes that you are aware about what is API polling in RESTful architecture.
3. All the code described is sort of a pseudo code, it's main purpose is to provide higher level understanding. The implementation might depend on the framework that you use.

With that said, let's get into it.

## Polling (The Task)
So let's say that we have a requirement where the client(UI) requires frequent updates for a certain section but it doesn't necessarily need to be in real time, for that case we can utilise API polling.

## setInterval() (The Intuitive Solution)
So we need a way to call an API endpoint repeatedly after a certain amount of interval(delay). 
Which JavaScript function can we use for this? "psst...it's setInterval()"

### API polling
We can achieve API polling by utilising `setInterval()` to repeatedly call fetch function.

```js
async function getData() {
  try {
      const response = await fetch(/*api endpoint*/);
      /* handle response here */
    } catch (error) {
      /* handle error case here */
    }
}

function apiPolling() {
  setInterval(getData, delay);
}

/* Start Polling */
apiPolling();
```
the client will repeatedly call the endpoint after a delay between each call where the delay is equal to the desired polling interval.

### Stop or Cancel polling
When we have to stop the polling at a certain condition or cancel the polling on user's request, we have to take care of two things.
1. Clear the setInterval function from queue.
2. Cancel any ongoing HTTP request that hasn't responded yet.

We can achieve this using `clearInterval` and `AbortController`.

```js
let intervalId;
let controller;
async function getData() {
  controller = new AbortController();
  const signal = controller.signal;
  try {
      const response = await fetch(/*api endpoint*/,
       { signal }
       );
      /* handle response here */
    } catch (error) {
      /* handle error case here */
    }
}

function apiPolling() {
  intervalId = setInterval(getData, delay);
}

/* Start Polling */
apiPolling();

function clearPolling() {
  clearInterval(intervalId);
  if (controller) {
    controller.abort();
  }
}

/* To stop or cancel polling */
clearPolling()
```

we clear the polling queue by calling the `clearInterval` function and making sure that any in-progress request is cancelled using `AbortController`. You can read more about [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)

## Issues with setInterval()

Intuitively setInterval might look like the best solution for polling until you stop and think about how it works. It **always** calls the function repeatedly after a **fixed delay**. This is might and will cause a host of issues, since polling in never just about calling the endpoint again again but requires careful consideration on how to handle error cases and when to poll for the next request.
Some of the issues are:
1. **Network latency** or **Unresponsive server** can cause the actual time to response to be greater than the polling interval, this can lead to network congestion and increased traffic on already struggling client or server.
2. **Variations in response time** in conjunction with the above issue can cause multiple queued up requests that won't necessarily return in order, leading to a host of request handling and UX problems.
3. Since setInterval isn't aware of the error case or stop condition, we have to **manually stop** the polling for every case and with the queued up request issue it might not be simple or even possible to cancel all those requests.


![Illustration showing api polling issues that can arise due to setInterval](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqkb3soyay65vsknwkwg.png)

## setTimeout() (The Proper Solution)
Intuitively `setTimeout` might not look like a solution for the polling at first. After all it only calls the function once after the delay and done.
How can we use setTimeout for a task that requires a function to be called repeatedly? "Ahh...the answer is recursion".

We can recursively call `setTimeout` to achieve polling and just by the virtue of how this solution works, we can improve upon the issues encountered while using setInterval and improve user experience as well as the developer experience (talking about DX, such an original of me).

Let's start with creating a recursive `setTimeout` function that loops over itself and is invoked immediately.

```js
(function loop() {
  setTimeout(() => {
   /* Function logic here */

    loop();
  }, delay);
})();
```

Now we can extract the function logic as a named function out of `setTimeout`.

```js
function loopingLogic() {
  /* Function logic here */
  loop();
}

(function loop() {
  setTimeout(loopingLogic, delay);
})();
```

Let's look at what's happening here, at first the `loop()` is called, it invokes a `setTimeout` which waits for the delay and calls the `loopingLogic()`, once all the tasks in that function are performed, at the end `loop()` function is called again, creating a recursion which calls the `loop()` after every execution of `loopingLogic()` 

Now let's say we only want to loop over the function if a certain condition is met or to stop looping when that condition is not met. We can achieve that by checking if that condition is true and only then continue to loop.

```js
function loopingLogic() {
  /* Function logic here */
  if (/* Condition is true */) {
    loop();
  }
}

(function loop() {
  setTimeout(loopingLogic, delay);
})();
```

Now, as we have the base logic setup, let's look at how we can achieve API polling using this pattern.

```js
async function getData() {
  try {
    const response = await fetch(/*api endpoint*/);
    /* handle response here */
    if (/* Condition is true */) {
      apiPolling();
    }
  } catch (error) {
    /* handle error case here */
  }
}

function apiPolling() {
  setTimeout(getData, delay);
}

/* Start Polling */
apiPolling();
```
Let's walkthrough the code. We can start polling by calling the `apiPolling()` function, it invokes a `setTimeout` which waits for the delay before calling the `getData()` function. 

In `getData()` function we are handling the data fetching logic. We are doing few things in that function:
1. We are hitting the api endpoint using fetch.
2. We wait for the response and handle the response accordingly.
3. We check if certain condition is met before calling the `apiPolling()` function to recursively hit the api endpoint for polling.
4. We catch any errors and handle those errors accordingly.

Now let's look at how this pattern of polling improves over `setInterval`.
1. We only poll for the next request when the response from the current request is received. This solves the problem of **network latency** and **unresponsive server** causing a network congestion, reduced overall traffic and mitigates the issue of race conditions caused by requests that won't necessarily return in order.
2. By checking for the base case or guard clause in the if statement, we can make sure the polling continues only if that condition is met, making it very easy to automatically stop polling and preventing other issues that might arise when something errors out and helps in better error handling.

We can expand over our new polling pattern to allow users to gracefully stop or cancel polling.

```js
let intervalId;
let controller;

async function getData() {
  controller = new AbortController();
  const signal = controller.signal;
  try {
    const response = await fetch(/*api endpoint*/,
    { signal }
    );
    /* handle response here */
    if (/* Condition is true */) {
      apiPolling();
    }
  } catch (error) {
    /* handle error case here */
  }
}

function apiPolling() {
  intervalId = setTimeout(getData, delay);
}

function clearPolling() {
  clearInterval(intervalId);
  if (controller) {
    controller.abort();
  }
}

/* To stop or cancel polling */
clearPolling()
```

## Conclusion

When presented with a task to create API polling, `setInterval` might intuitively seem like the best option for this task. However as discussed above we can see that it is not the best option and can lead to some nasty issues and complicated code as we progress. Instead we can utilise the **recursive `setTimeout` pattern** to create better a solution and prevent or even improve upon the issues that might arise by using `setInterval`.

Remember, whenever you have to poll an API on a remote server, always go for **recursive `setTimeout` pattern**.

Let me know in the comments below if you found this post helpful, any points that I missed and discuss what are the different ways you like to do API polling.

I like to talk about tech in general and events surrounding it. You can connect with me on [Twitter/X](https://x.com/idrisGadiX) and [LinkedIn](https://www.linkedin.com/in/idris-gadi/)