DEV Community

Aks
Aks

Posted on • Edited on

Why not to use setInterval

Recently, I came across a requirement where I had to call a function repeatedly after specific time interval, like sending ajax call at every 10 seconds. Sure, best option seems as setInterval, but it blew up my face like a cracker :)

In order to understand why setInterval is evil we need to keep in mind a fact that javascript is essentially single threaded, meaning it will not perform more than one operation at a time.

In cases when functions takes longer than delay mentioned in setInterval (like ajax call, which might it prevent from completing on time), we will find that either functions have no breathing room or setInterval breaks it's rhythm.

    var fakeCallToServer = function() {
        setTimeout(function() {
            console.log('returning from server', new Date().toLocaleTimeString());
        }, 4000);
    }



    setInterval(function(){ 

        let insideSetInterval = new Date().toLocaleTimeString();

        console.log('insideSetInterval', insideSetInterval);

        fakeCallToServer();
    }, 2000);

//insideSetInterval 14:13:47
//insideSetInterval 14:13:49
//insideSetInterval 14:13:51
//returning from server 14:13:51
//insideSetInterval 14:13:53
//returning from server 14:13:53 
//insideSetInterval 14:13:55
//returning from server 14:13:55

Enter fullscreen mode Exit fullscreen mode

Try above code snippets in your console

As you can see from printed console.log statement that setInterval keeps on sending ajax calls relentlessly without caring previous call has returned or not.
This can queue up a lot of requests at once on the server.

Now, let's try a synchronous operation in setInterval:

var counter = 0;

var fakeTimeIntensiveOperation = function() {

    for(var i =0; i< 50000000; i++) {
        document.getElementById('random');
    }

    let insideTimeTakingFunction  = new Date().toLocaleTimeString();

    console.log('insideTimeTakingFunction', insideTimeTakingFunction);
}



var timer = setInterval(function(){ 

    let insideSetInterval = new Date().toLocaleTimeString();

    console.log('insideSetInterval', insideSetInterval);

    counter++;
    if(counter == 1){
        fakeTimeIntensiveOperation();
    }

    if (counter >= 5) {
       clearInterval(timer);
    }
}, 1000);

//insideSetInterval 13:50:53
//insideTimeTakingFunction 13:50:55
//insideSetInterval 13:50:55 <---- not called after 1s
//insideSetInterval 13:50:56
//insideSetInterval 13:50:57
//insideSetInterval 13:50:58

Enter fullscreen mode Exit fullscreen mode

We see here when setInterval encounters time intensive operation, it does either of two things, a) try to get on track or b) create new rhythm. Here on chrome it creates a new rhythm.

Conclusion

In case of asynchronous operations, setTimeInterval will create long queue of requests which will be very counterproductive.
In case of time intensive synchronous operations, setTimeInterval may break the rhythm.
Also, if any error occurs in setInterval code block, it will not stop execution but keeps on running faulty code. Not to mention they need a clearInterval function to stop it.
Alternatively, you can use setTimeout recursively in case of time sensitive operations.

Top comments (16)

Collapse
 
ycmjason profile image
YCM Jason • Edited

How about making an async version of setInterval? Something like...

const setIntervalAsync = (fn, ms) => {
  fn().then(() => {
    setTimeout(() => setIntervalAsync(fn, ms), ms);
  });
};
Enter fullscreen mode Exit fullscreen mode

So we can just do

setIntervalAsync(() => fetch(/* blah */), 3000);
Enter fullscreen mode Exit fullscreen mode

Which would call fetch every 3000ms properly.

P.S. Haven't really tested it, just an idea...

Collapse
 
sirius1024 profile image
Guo Tuo

And handle errors:

const delayReport = deplayMs => new Promise((resolve) => {
    setTimeout(resolve, deplayMs);
});

setIntervalAsync(async () => {
    try {
        const seed = Math.floor((Math.random() * 100) + 1);
        if (seed % 2 === 0) {
            console.log(new Date());
            await delayReport(500);
        } else { throw new Error('get a random error'); }
    } catch (e) {
        console.error(e);
    }
}, 1000);

Enter fullscreen mode Exit fullscreen mode
Collapse
 
sirius1024 profile image
Guo Tuo • Edited
const delayReport = deplayMs => new Promise((resolve) => {
    setTimeout(resolve, deplayMs);
});

setIntervalAsync(async () => { console.log(new Date()); await delayReport(1000); }, 1000);
Enter fullscreen mode Exit fullscreen mode

The print is

2018-08-26 09:13:43
2018-08-26 09:13:45
2018-08-26 09:13:47
2018-08-26 09:13:49
2018-08-26 09:13:51
2018-08-26 09:13:53

Cool man!

Collapse
 
akanksha_9560 profile image
Aks

Will test it and let you know. :)

Collapse
 
coyr profile image
Alejandro Barrero

Hello, I am curious If you tested it :)

Collapse
 
moopet profile image
Ben Sinclair • Edited

Another approach is to check that the previous process isn't still running inside the setInterval callback and choose whether to skip that iteration or to kill and restart the timer.

Better would be to trigger actions on user events rather than timed events if that's possible, or to use timers only for pure cosmetics. I don't think setInterval/setTimeout are evil as such, as long as you're careful since suddenly everything's in global scope.

There's a temptation to use them to get out of a fix, though, and I think you're definitely right to treat them as a bit of a smell.

Collapse
 
ne0nx3r0 profile image
Robert Manzano

Yeah typically I'd expect either a conditional in your response function.

That's also generally how Redux ajax requests are patterned with Redux-Thunk, you would have a conditional in your response handling to verify if it still makes sense to replace the state.

Collapse
 
stevetaylor profile image
Steve Taylor

There are perfectly sane ways of using setInterval without requests backing up, e.g. (warning: untested RxJS code ahead):

interval(10000)
    .switchMap(() => Observable.create(observer => {
        const request = superagent.get(
            'https://example.com/api/v1/foo',
            (err, res) => {
                if (!err) {
                    observer.next(res.body)
                }

                observer.complete()
            }
        )

        return () => request.abort()
    }))
    .subscribe(console.log)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
belkamax05 profile image
belkamax05

As you can see from printed console.log statement that whole load of setInterval events arrive immediately after each other without any delay.

Didn't get a point, as I see from printed console, results are fully predicted. Every single "insideSetInterval" prints with delay of two seconds (like described in setInterval), and also - "returning from server" appears after 4 seconds since first setInterval, which was called with timeout 4000ms, where is "without any delay"?

If you speak about no delay between "returning from server" and "insideSetInterval", of course, because setTimeout with 4sec will wait until interval will be executed twice), and dont have to be in sync with interval prints. With heavy calculation it can load stack, but for simple refreshes - like a timer display update or some other light calculations - working properly, if not forget to handle instance with "clearInterval" when it's no needed anymore.

Collapse
 
akanksha_9560 profile image
Aks

You are right, for light calculations it does not make much of a difference. But for example gmail has to refresh your inbox when new mail arrives, if we were to do it by setTimeInterval, we will queue a lot of requests at server. I have changed the contents of the blog. Please go through that maybe it will put across my point clearly.

Collapse
 
belkamax05 profile image
belkamax05

Thanks for your reply, yes. I agree, if there will be heavy calculation or long time awaiting, it will work unpredictable. For heavy calculations I would like to use some kind of state machines, which will not make additional request if previous one still in pending state (can be timeout of it). Unfortunatelly, we cannot stop executing request to a server, but we can mark it in client side as NOT VALID (throw exception after some timeout), but this will continue execution on server and in case of non-stop requests, they will do DoS attack. But that's also not a setInterval and setTimeout case, there have to be much more code over it, to prevent that kind of issues. If server taking decision when content have to be updated - WebSockets can prevent lot of issues, IMHO they are ready to be used in production.

Collapse
 
juanmendes profile image
juanmendes • Edited

Another reason is because setIntervals stop running if your browser window loses focus, your callbacks will queue up and run once the page has focus again. Or even worse, they will be dropped. See jsfiddle.net/mendesjuan/y3h1n6mk/

Collapse
 
antialias profile image
Thomas Hallock

In order to understand why setInterval is evil

Thanks for the detailed explanation of setInterval in sync and async contexts, but why is it this "evil"? With a proper understanding of how the js event loop works, it's easy to understand the reason why a "new rhythm" gets established in the sync case, and why it doesn't in the async case.

Collapse
 
tea247 profile image
Covenant T. Junior

You don't need to create the variable, but it's a good practice as you can use that variable with clearInterval to stop the currently running interval.

var int = setInterval("doSomething()", 5000 ); /* 5 seconds /
var int = setInterval(doSomething, 5000 ); /
same thing, no quotes, no parens */

If you need to pass parameters to the doSomething function, you can pass them as additional parameters beyond the first two to setInterval.

Without overlapping

setInterval, as above, will run every 5 seconds (or whatever you set it to) no matter what. Even if the function doSomething() takes long than 5 seconds to run. That can create issues. If you just want to make sure there is that pause in between runnings of doSomething, you can do this:
(function(){
doSomething();
setTimeout(arguments.callee, 5000);
})()

Collapse
 
tea247 profile image
Covenant T. Junior

For My Chat App
function messageCount(){
var bond=$('#active-friend-status').attr('bond');
if (bond!='') {
$.ajax({
url : 'messageCount',
type : 'GET',
data : {'messageCount' : true, 'bond' : bond},
success : function(data) {
var messageCount = parseInt(data);
newCount(messageCount);
},
error : function() {
}
})

}
function update(messageCount, newCount) {
var bond=$('#active-friend-status').attr('bond');
if (messageCount<newCount) {
$('#active-message-box').load('message?bond='+bond+'');
console.log('Updated');

}
}
function newCount(messageCount) {
var m = messageCount;
setTimeout(function newCount(m) {
var bond=$('#active-friend-status').attr('bond');
if (bond!=''){
$.ajax({
url : 'messageCount',
type : 'GET',
data : {'messageCount' : true, 'bond' : bond},
success : function(data) {
var newCount = parseInt(data);

update(messageCount, newCount);
},
error : function() {
}
})
}
}, 3000);
}

}

(function(){
messageCount();
setTimeout(arguments.callee, 2000);
})()

Collapse
 
prafullsalunke profile image
Prafull Salunke

I get what you are trying to say here. A small mistake here though. The timeout in fakeCallToServer should have been a random from 200ms to 15 sec to mimic an actual api response scenario.