64

I have the following code:

for(var i = 0; i < list.length; i++){
    mc_cli.get(list[i], function(err, response) {
        do_something(i);
    });
}

mc_cli is a connection to a memcached database. As you can imagine, the callback function is asynchronous, thus it may be executed when the for loop already ended. Also, when calling in this way do_something(i) it always uses the last value of the for loop.

I tried with a closure in this way

do_something((function(x){return x})(i)) 

but apparently this is again using always the last value of the index of the for loop.

I also tried declaring a function before the for loop like so:

var create_closure = function(i) {
    return function() {
        return i;
    }
}

and then calling

do_something(create_closure(i)())

but again without success, with the return value always being the last value of the for loop.

Can anybody tell me what am I doing wrong with closures? I thought I understood them but I can't figure why this is not working.

Masiar
  • 20,450
  • 31
  • 97
  • 140

10 Answers10

83

Since you're running through an array, you can simply use forEach which provides the list item, and the index in the callback. Iteration will have its own scope.

list.forEach(function(listItem, index){
  mc_cli.get(listItem, function(err, response) {
    do_something(index);
  });
});
Joseph
  • 117,725
  • 30
  • 181
  • 234
  • 2
    @joseph you reasoning sounds great. Can you explain this part of me please " Iteration will have its own scope" ? – Sandip Subedi Apr 14 '17 at 11:25
  • 5
    @SandipSubedi - This is where the concept of closures in javascript kicks-in. It's the ability of the inner function scope to remember all its references even if they are outside the function scope. You can view closures in dev tools using console.dir(object) or may be by simply putting breakpoint at the 'do_something' function and viewing the right pane of the developer tools. You can read about this more here => https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures. – Bose_geek May 12 '18 at 18:43
46

This is the asynchronous-function-inside-a-loop paradigm, and I usually deal with it using an immediately-invoked-anonymous-function. This ensures that the asynchronous functions get called with the correct value of the index variable.

Okay, great. So all the asynchronous functions have been started up, and the loop exits. Now, there is no telling when these functions will complete, due to their asynchronous nature, or in what order they will complete. If you have code that needs to wait until all these functions have completed before executing, I recommend keeping a simple count of how many functions have finished:

var total = parsed_result.list.length;
var count = 0;

for(var i = 0; i < total; i++){
    (function(foo){
        mc_cli.get(parsed_result.list[foo], function(err, response) {
            do_something(foo);
            count++;
            if (count > total - 1) done();
        });
    }(i));
}

// You can guarantee that this function will not be called until ALL of the
// asynchronous functions have completed.
function done() {
    console.log('All data has been loaded :).');
}
user3707766
  • 517
  • 4
  • 3
20

I know this is a old thread but anyway adding my answer. ES2015 let has the feature of rebinding the loop variable on each iteration, so it maintains the value of loop variable in asynchronous callbacks, so you can try the below one:

for(let i = 0; i < list.length; i++){
    mc_cli.get(list[i], function(err, response) {
        do_something(i);
    });
}

But anyway, it's better to use forEach or create a closure using immediately-invoked-function, since let is ES2015 feature and might not be support all browsers and implementations. From here under Bindings ->let->for/for-in loop iteration scope I can see it isn't supported till Edge 13 and even till Firefox 49 (I haven't checked in these browsers). It even says it's not supported with Node 4, but I personally tested and it seems it is supported.

tmthydvnprt
  • 10,398
  • 8
  • 52
  • 72
vikneshwar
  • 597
  • 1
  • 6
  • 17
  • 5
    I have been banging my head on a wall for past 24 hours, because I couldn't figure out why the f**king `for loop` isn't working as it suppose to. I have been using `var i = 0` this whole time until I see your post. I change `var i = 0` to `let i = 0` and everything magically works fine. How can I give you all my reputation, you deserve all of it... – Hafiz Temuri Aug 08 '17 at 15:45
  • 2
    `let` was the winner for me – Jeff Beagley Jun 15 '18 at 16:01
14

You were pretty close, but you should pass the closure to get instead of putting it inside the callback:

function createCallback(i) {
    return function(){
        do_something(i);
    }
}


for(var i = 0; i < list.length; i++){
    mc_cli.get(list[i], createCallback(i));
}
Dennis
  • 32,200
  • 11
  • 64
  • 79
6

Try this, using the async/await syntax and Promise

(async function() {
    for(var i = 0; i < list.length; i++){
        await new Promise(next => {
            mc_cli.get(list[i], function(err, response) {
                do_something(i); next()
            })
        })
    }
})()

This will stop the loop in each cycle until the next() function is triggered

Fernando Carvajal
  • 1,869
  • 20
  • 19
2

ES2017: You can wrap the async code inside a function(say XHRPost) returning a promise( Async code inside the promise).

Then call the function(XHRPost) inside the for loop but with the magical Await keyword. :)

let http = new XMLHttpRequest();
let url = 'http://sumersin/forum.social.json';

function XHRpost(i) {
    return new Promise(function(resolve) {
        let params = 'id=nobot&%3Aoperation=social%3AcreateForumPost&subject=Demo' + i + '&message=Here%20is%20the%20Demo&_charset_=UTF-8';
        http.open('POST', url, true);
        http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        http.onreadystatechange = function() {
                console.log("Done " + i + "<<<<>>>>>" + http.readyState);
                if(http.readyState == 4){
                    console.log('SUCCESS :',i);
                    resolve();
                }
        }
        http.send(params);       
   });
}

for (let i = 1; i < 5; i++) {
    await XHRpost(i);
   }
Sumer
  • 2,687
  • 24
  • 24
0

If you want to run asynchronous functions inside a loop, but still want to keep the index or other variables after a callback gets executed you can wrap your code in an IIFE (immediately-invoked function expression).

var arr = ['Hello', 'World', 'Javascript', 'Async', ':)'];
for( var i = 0; i < arr.length; i++) {
  (function(index){
    setTimeout(function(){
       console.log(arr[index]);
 }, 500);
Amay Kulkarni
  • 828
  • 13
  • 16
0
(async function() {
  for(var i = 1; i < category.length; i++){

   let result = await $.ajax({
        url: '{{ route('admin.loyalty-commission.sub-category') }}', // Url of the Route
        
        data: {id:category[i-1], cat_state:i, next_cat:category[i]},
        success: function (data) {
            console.log('ajax success previous category id' + category[i-1]);

            // Check if the logic was successful or not
            if (data.status == 'success') {
                
                $(data.subcategories).appendTo(sub_category); //ajax result append to sub_category
            } else {
                console.log(data.msg);
            }
        },
        error: function (data) {
            // Error while calling the controller (HTTP Response Code different as 200 OK
            console.log('Error:', data);
            success = false;
        }
    });
 }
})()
razan rai
  • 1
  • 1
-1

This is a sample code from my application. It might help solve the issue. I used async/await in a map loop. multiple promises resolved in one array.

This helped me solve this problem JavaScript async and await in loops

 const refObjId= ['Account', 'Contact', 'Group'];

 const readdirPro = file => {
   return new Promise((resolve, reject) => {
    fs.readdir(file, (err, data) => {
      if (err) reject('I could not find the file');
      resolve(data);
    });
  });
};

const fileNamePromises = refObjId.map(async el => {
    const fileName = await readdirPro(`${__dirname}/../csv-files/${el}/data`);
    return fileName;
  });

//fileNamePromises is an array of promises


  const fileArr = await Promise.all(fileNamePromises);
    console.log(fileArr);
Hossain
  • 1
  • 1
-2

Using ES6 (typescript) you can use the benefits of async and await:

let list: number[] = [1, 2, 3, 4, 5];

// this is async fucntion
function do_something(counter: number): Promise<number> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('called after ' + counter + ' seconds');
            resolve(counter);
        }, counter * 1000);
    })
}
async function foo() {
    // itrate over list and wait for when everything is finished
    let data = await Promise.all(list.map(async i => await do_something(i)));

    console.log(data);
}

foo();
Jesse
  • 3,522
  • 6
  • 25
  • 40
Gyan
  • 140
  • 2
  • 11