322

I have this script:

for (var i = 1; i <= 2; i++) {
    setTimeout(function() { alert(i) }, 100);
}

But 3 is alerted both times, instead of 1 then 2.

Is there a way to pass i, without writing the function as a string?

royhowie
  • 11,075
  • 14
  • 50
  • 67
Ilyssis
  • 4,849
  • 7
  • 24
  • 30
  • 9
    None of the answers here work. Each of them simply delays for the set time, and then immediately runs the entire loop without further delays. Looking at the OP's code, they clearly wanted a delay with each iteration. – Chuck Le Butt Feb 24 '15 at 13:54
  • 1
    It's also worth noting that if the user DID want the alerts to fire at the same time, setting up of multiple `setTimeout`s is NOT the best way to do it. – Chuck Le Butt Feb 24 '15 at 15:54
  • 13
    use "let" keyword instead of var, this will resolve. – Varadha31590 Nov 24 '16 at 09:40
  • 9
    I was trying something similar and no one was able to answer the question, or explain what it is I was doing wrong, conceptually. Here's what you need to understand, probably: setTimeout() is asyncrhronous: The JS engine will not wait n milliseconds (100 in your example), before proceeding. It's just makes a 'mental note' : "After 100 ms, execute (in this case) the Alert", and continues executing the loop. It does all 3 (or 300) iterations before the 100ms is over, so eventually, when that time DOES elapse, it spits out all 3 (or 300) alerts at once). – PakiPat Jun 20 '17 at 16:41
  • 2
    I think you can use `let` instead of `var` .This will solve your problem – Mari Selvan Jun 09 '19 at 11:34
  • 2
    How let will solve problem ? Can you clarify – Tejas Feb 25 '20 at 07:17
  • @ChuckLeButt yes you are right, here I wrote a code that actually WORKS, function easyLoopLimiter(loopStrtNum, loopEndNum, timeToPause, yourFunction) { function loop(i) { if (i{i++;loop(i)}, timeToPause); } else {return;} } setTimeout(()=>{loop(loopStrtNum)}, timeToPause); } /* Here an example */ easyLoopLimiter(0, 10, 1000, "alert(i); console.log(i)") – Sevada 797 Aug 06 '23 at 14:16
  • Also don't try it in "New tab" console cause it will throw an error because of Google's security – Sevada 797 Aug 06 '23 at 14:37

10 Answers10

426

You have to arrange for a distinct copy of "i" to be present for each of the timeout functions.

function doSetTimeout(i) {
  setTimeout(function() {
    alert(i);
  }, 100);
}

for (var i = 1; i <= 2; ++i)
  doSetTimeout(i);

If you don't do something like this (and there are other variations on this same idea), then each of the timer handler functions will share the same variable "i". When the loop is finished, what's the value of "i"? It's 3! By using an intermediating function, a copy of the value of the variable is made. Since the timeout handler is created in the context of that copy, it has its own private "i" to use.

Edit:

There have been a couple of comments over time in which some confusion was evident over the fact that setting up a few timeouts causes the handlers to all fire at the same time. It's important to understand that the process of setting up the timer — the calls to setTimeout() — take almost no time at all. That is, telling the system, "Please call this function after 1000 milliseconds" will return almost immediately, as the process of installing the timeout request in the timer queue is very fast.

Thus, if a succession of timeout requests is made, as is the case in the code in the OP and in my answer, and the time delay value is the same for each one, then once that amount of time has elapsed all the timer handlers will be called one after another in rapid succession.

If what you need is for the handlers to be called at intervals, you can either use setInterval(), which is called exactly like setTimeout() but which will fire more than once after repeated delays of the requested amount, or instead you can establish the timeouts and multiply the time value by your iteration counter. That is, to modify my example code:

function doScaledTimeout(i) {
 setTimeout(function() {
   alert(I);
 }, i * 5000);
}

(With a 100 millisecond timeout, the effect won't be very obvious, so I bumped the number up to 5000.) The value of i is multiplied by the base delay value, so calling that 5 times in a loop will result in delays of 5 seconds, 10 seconds, 15 seconds, 20 seconds, and 25 seconds.

Update

Here in 2018, there is a simpler alternative. With the new ability to declare variables in scopes more narrow than functions, the original code would work if so modified:

for (let i = 1; i <= 2; i++) {
  setTimeout(function() {
    alert(i)
  }, 100);
}

The let declaration, unlike var, will itself cause there to be a distinct i for each iteration of the loop.

innocent
  • 864
  • 6
  • 17
Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 7
    This is the preferred method as it does not cause a function definition inside the body of the loop. The others will work, but are not preferable (even if they do show the amazing bad-assness of JS ;) ). – JAAulde Mar 07 '11 at 23:03
  • 1
    @JAAulde I confess that I personally would do it with an anonymous function, but this way is nicer as an example. – Pointy Mar 07 '11 at 23:05
  • 1
    I personally like anonymous functions more than this because I don't want to set up a bunch of names. Too lazy think of them. – Derek 朕會功夫 Jul 19 '12 at 19:28
  • 4
    @Pointy : This is not working for me, the js waits for 100 and then whole for loop is executed at once. please correct me if I am doing something wrong. – PG1 Jun 18 '14 at 06:05
  • @ParagGangil I have no idea what is wrong with your code because I cannot see it. Post a new question, and include your code. – Pointy Jun 18 '14 at 10:10
  • function doSetTimeout(i) { setTimeout(function() { console.log(i); }, 1000); } for (var i = 1; i < 10; ++i) doSetTimeout(i); – PG1 Jun 18 '14 at 10:15
  • @ParagGangil each one of the timeouts will be set up to happen 1 second in the future, but they're all set up at almost the same time. Thus, 1 second after your `for` loop runs, all of the timers will fire. – Pointy Jun 18 '14 at 10:38
  • This does not work: http://jsfiddle.net/ubruksco/ – Chuck Le Butt Feb 24 '15 at 13:50
  • @Chuck your fiddle looks like it works to me; what did you expect it to do? – Pointy Feb 24 '15 at 14:47
  • @Chuck note that all of your calls to *set up* the timeouts will happen at almost exactly the same time. The effect of your `for` loop is to start 5 timers that will all fire 5 seconds after that point, again essentially at the same time. If you want the timers to fire at 5 second intervals, you could use an interval timer, or else multiply 5000 by the value of `i` for the timeout duration. – Pointy Feb 24 '15 at 14:51
  • @Pointy If you look at the OPs question, it seems clear to me they want a delay -- otherwise why even have a loop? – Chuck Le Butt Feb 24 '15 at 15:48
  • 1
    @Chuck I agree that it's unlikely that the OP wanted both alerts to go off at the same time, but I don't know for sure. The thrust of the question involved the fact that the *value* in the alert was unexpected (3 instead of 1 and 2). I've extended my answer to discuss the fact that setting up multiple timeouts at the same time will cause them all to fire at the same time if the delays are the same. – Pointy Feb 24 '15 at 15:49
  • @Pointy It's worth noting that if the OP didn't need a delay between each loop, then there are better ways of achieving the same result -- for a start, removing the setting up of multiple setTimeouts, which is bad practice. Anyways, good job on updating your answer. – Chuck Le Butt Feb 24 '15 at 15:51
  • is it possible to do this without an external function? – tofutim Jul 15 '16 at 07:24
  • why setting setTimeout function time equal 0 gives the same output? see here https://jsfiddle.net/sahilsolanki07/1sst1yk2/ – sahil solanki Oct 20 '16 at 14:02
  • @sahilsolanki even with a timeout value of 0, the function is deferred; it is not called immediately. – Pointy Oct 20 '16 at 14:10
  • @Pointy, in respond to ParagGangil you said all timeout will fire at the same time(5 sec) there, In this case, shouldn't it fired at 0th sec i.e when i is equal to 1. – sahil solanki Oct 20 '16 at 14:23
  • 1
    @sahilsolanki no, the timeouts won't fire until after the `for` loop completes. A `setTimeout()` call always defers execution of the function, even if the timeout is zero seconds. – Pointy Oct 20 '16 at 14:27
  • @ParagGangil is correct, it simply executes everything at once. Since this is marked as the correct answer, I'll have to add my 2¢ in the comments. In my opinion, the best way to solved this is by using setInterval and clearInterval to loop through your iterable while incrementing your index: `let a = ['one', 'two', 'three'], i = 0; let timer = setInterval(() => { console.log(a[i]); i++; if( i >= a.length ) { clearInterval(timer); }}, 1000);` Copy and paste that into your console to try it. – Dan S Apr 12 '22 at 21:29
198

You can use an immediately-invoked function expression (IIFE) to create a closure around setTimeout:

for (var i = 1; i <= 3; i++) {
    (function(index) {
        setTimeout(function() { alert(index); }, i * 1000);
    })(i);
}
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    This does not work: http://jsfiddle.net/Ljr9fq88/ – Chuck Le Butt Feb 24 '15 at 13:48
  • 2
    IIFE is nothing but a convenient shortcut for no-named function and immediate execution- Thats what Accepted answer does actually, in full steps, no shortcut- wraps the function call in another function, so inner function gets local copy of outer function arguement.!!! – Karan Kaw May 17 '17 at 10:13
  • The answer is incorrect. The closure was already there since you could call `alert(i)` from an anonymous callback. The problem was closure references `i` from global `for` block. So the proper answer is: IIFE creates additional scope per iteration to bound `i` and pass it to the anonymous callback. Then closure references `i` from local iteration scope. – klimat Oct 27 '17 at 14:49
  • 1
    My shortest way: Use ```let``` instead of ```var``` in for loop – hien May 05 '18 at 16:16
  • This works, but the timeout is not multiplied. It runs every second for each of those loops. Tested in jsfiddle and here using "Run code snippet". – Jan Horčička May 10 '18 at 10:01
  • 2
    Yours was the only one that worked for me, I didn't want all the requests to be run altogether, one after the other without waiting. I wanted to run each event after certain amount of time. Thanks! – Edenshaw Apr 01 '20 at 13:28
  • 1
    Same here, the only one that works well for me ! Thanks ! – PalmThreeStudio Feb 24 '22 at 12:19
43

This's Because!

  1. The timeout function callbacks are all running well after the completion of the loop. In fact, as timers go, even if it was setTimeout(.., 0) on each iteration, all those function callbacks would still run strictly after the completion of the loop, that's why 3 was reflected!
  2. all two of those functions, though they are defined separately in each loop iteration, are closed over the same shared global scope, which has, in fact, only one i in it.

the Solution's declaring a single scope for each iteration by using a self-function executed(anonymous one or better IIFE) and having a copy of i in it, like this:

for (var i = 1; i <= 2; i++) {

     (function(){

         var j = i;
         setTimeout(function() { console.log(j) }, 100);

     })();

}

the cleaner one would be

for (var i = 1; i <= 2; i++) {

     (function(i){ 

         setTimeout(function() { console.log(i) }, 100);

     })(i);

}

The use of an IIFE(self-executed function) inside each iteration created a new scope for each iteration, which gave our timeout function callbacks the opportunity to close over a new scope for each iteration, one which had a variable with the right per-iteration value in it for us to access.

Mehdi Raash
  • 8,721
  • 2
  • 29
  • 42
  • when I set the i variable to a bigger number(3 or bigger),number order it alerts get strange.Can you explain me why?it's because of setTimeout or alert?thanks a lot. – Oboo Cheng Oct 15 '16 at 15:51
  • Thank you, because of the problem you said I changed the **alert()** to **console.log()** for demonstration sake. At least in chrome it works Okay! and about the problem please check this question out [Question](http://stackoverflow.com/questions/40073278/google-chromes-unpredictable-behaviour-on-alert-function) – Mehdi Raash Dec 01 '16 at 13:39
  • Beautifully demonstrated!! – Badal Saibo Sep 13 '22 at 18:42
27

The function argument to setTimeout is closing over the loop variable. The loop finishes before the first timeout and displays the current value of i, which is 3.

Because JavaScript variables only have function scope, the solution is to pass the loop variable to a function that sets the timeout. You can declare and call such a function like this:

for (var i = 1; i <= 2; i++) {
    (function (x) {
        setTimeout(function () { alert(x); }, 100);
    })(i);
}
harto
  • 89,823
  • 9
  • 47
  • 61
  • This does not work: http://jsfiddle.net/sq5n52xj/ – Chuck Le Butt Feb 24 '15 at 13:52
  • 3
    To work, it just needs to multiply the delay with i. like this: setTimeout(function () { alert(x); }, i*100); – Placid Oct 06 '15 at 16:28
  • You just need to replace var with let keyword and it will print the number from 1 and then 2. But here is the catch again, this will print both 1 and 2 just after the 2 sec only. If you want to print 1 and 2 at an interval of 1 sec each, then in setTimeout callback, modify 1000 to `i * 1000` – Pushp Singh Jun 23 '21 at 08:05
23

You can use the extra arguments to setTimeout to pass parameters to the callback function.

for (var i = 1; i <= 2; i++) {
        setTimeout(function(j) { alert(j) }, 100, i);
}

Note: This doesn't work on IE9 and below browsers.

Aamir Afridi
  • 6,364
  • 3
  • 42
  • 42
Mevin Babu
  • 2,405
  • 21
  • 34
9

ANSWER?

I'm using it for an animation for adding items to a cart - a cart icon floats to the cart area from the product "add" button, when clicked:

function addCartItem(opts) {
    for (var i=0; i<opts.qty; i++) {
        setTimeout(function() {
            console.log('ADDED ONE!');
        }, 1000*i);
    }
};

NOTE the duration is in unit times n epocs.

So starting at the the click moment, the animations start epoc (of EACH animation) is the product of each one-second-unit multiplied by the number of items.

epoc: https://en.wikipedia.org/wiki/Epoch_(reference_date)

Hope this helps!

Cody
  • 9,785
  • 4
  • 61
  • 46
5

You could use bind method

for (var i = 1, j = 1; i <= 3; i++, j++) {
    setTimeout(function() {
        alert(this);
    }.bind(i), j * 100);
}
Raghavendra
  • 5,281
  • 4
  • 36
  • 51
2

Well, another working solution based on Cody's answer but a little more general can be something like this:

function timedAlert(msg, timing){
    setTimeout(function(){
        alert(msg);    
    }, timing);
}

function yourFunction(time, counter){
    for (var i = 1; i <= counter; i++) {
        var msg = i, timing = i * time * 1000; //this is in seconds
        timedAlert (msg, timing);
    };
}

yourFunction(timeInSeconds, counter); // well here are the values of your choice.
maisafie
  • 21
  • 1
0

I had the same problem once this is how I solved it.

Suppose I want 12 delays with an interval of 2 secs

    function animate(i){
         myVar=setTimeout(function(){
            alert(i);
            if(i==12){
              clearTimeout(myVar);
              return;
            }
           animate(i+1)
         },2000)
    }

    var i=1; //i is the start point 1 to 12 that is
    animate(i); //1,2,3,4..12 will be alerted with 2 sec delay
Raj Nandan Sharma
  • 3,694
  • 3
  • 32
  • 42
  • `animate(i);` - `i` is undefined, this whole method will only work as described if you call `animinate(1)`, will not work correctly with any other value. The parameter is pointless at best. – nathanchere Sep 08 '16 at 11:22
  • This is supposed to create 12 consecutive delays. "i" here is to change the number of delays.. if 1 there will be 12. if 11 there will be two. – Raj Nandan Sharma Sep 08 '16 at 11:38
-21

the real solution is here, but you need to be familiar with PHP programing language. you must mix PHP and JAVASCRIPT orders in order to reach to your purpose.

pay attention to this :

<?php 
for($i=1;$i<=3;$i++){
echo "<script language='javascript' >
setTimeout(function(){alert('".$i."');},3000);  
</script>";
}
?> 

It exactly does what you want, but be careful about how to make ralation between PHP variables and JAVASCRIPT ones.

Shehary
  • 9,926
  • 10
  • 42
  • 71
reza
  • 1