Prostak
Prostak

Reputation: 3725

How to do a "for" loop with asynchronous condition in Javascript?

I have this function:

waitForFreeAccnt.prototype.isMemberFree = function () {
    var self = this;

    self.api.getMemberInfo(function () {
        var accType = self.api.connect.accountType;
        console.log(accType);
        if (accType === 'FREE') {
            console.log('it is free');
            return true;
        } else {
            console.log('it is not free');
            return false;
        }
    });
};

I would like to wait till the account is free for up to 10 seconds with something like that:

var test = function () {
    for (var start = 1; start < 10; start++) {
        var result = self.isMemberFree();
        console.log(result);
        if (result) {
            break;
        } else {
            self.api.pause(1000);
            console.log('waiting');
        }
    }
};

But it doesn't work because self.api.getMemberInfo is asynch call. This is super frustrating with Javascript. Any other language it would be so simple to do. How do I force the for loop to wait for self.isMemberFree() to finish executing before proceeding with the loop?

Also to note, this is not in browser execution so I don't care about anything hanging.

Upvotes: 1

Views: 227

Answers (4)

Nebula
Nebula

Reputation: 7151

Building on naomik's answer, if you do it that way you can pretty easily use a for loop with it, using the (most likely) upcoming async/await feature - though it's not part of ES2015.

// Note "async" here! That will make "await" work. It makes the function
// return a promise, which you'll be able to either "await" or
// "test().then" later.
var test = async function () {
    for (var start = 1; start < 10; start++) {
        // Right here we're using "await" - it makes JavaScript *wait* for
        // the promise that comes from self.isMemberFree() to be finished.
        // It's really handy because you can use it in loops like "for" and
        // "while" without changing the flow of your program!
        var result = await self.isMemberFree();
        console.log(result);
        if (result) {
            break;
        } else {
            self.api.pause(1000);
            console.log('waiting');
        }
    }
};

For now you'll need to use a transpiler like Babel or Traceur before you can really use async/await, though. It's only supported in Microsoft Edge 14 right now.

And a big emphasis that what is returned from test() isn't whatever you directly return from inside it. If I do this:

var test = async function() { return 15; };
var result = test();

I'm not going to get 15 - I'll get a promise that will resolve as 15:

result.then(function(res) {
  console.log(res); // 15
});

// or, use an async function again:
var main = async function() {
  console.log(await res); // 15
};
main();

Upvotes: 2

Mulan
Mulan

Reputation: 135197

You could return a Promise

waitForFreeAccnt.prototype.isMemberFree = function () {
  return new Promise((reject, resolve)=>
    // set a timeout if api call takes too long
    var timeout = setTimeout(()=> reject(Error('API timeout')), 10000);
    // make api call
    this.api.getMemberInfo(()=> {
      clearTimeout(timeout);
      resolve(this.api.connect.accountType === 'FREE');
    });
  );
};

Then use it like this

whatever.isMemberFree().then(isFree=> {
  if (isFree)
    console.log('it is free');
  else
    console.log('it is not free');
})
// handle timeout or other errors
.catch(err=> {
  console.log(err.message);
});

Upvotes: 2

nnnnnn
nnnnnn

Reputation: 150010

When dealing with asynchronous code, you need to make use of callbacks. That is, if you want to do a() and b() in order but a() does something asynchronously, then you need to call b() from within a() once a() has a result. So not:

a();  // does something asynchronously
b();  // tries to use a()'s result but it isn't available yet

... but rather

a(b); // pass b to a() and a() will call it when ready

function a(callback) {
  triggerAsyncFunction(function(result) {
    if (result === something)
      callback("a just finished");
  });
}

Note that a() doesn't refer to b() by name, it just calls whatever function is passed in as an argument.

So applying that to your code, maybe something like this:

waitForFreeAccnt.prototype.isMemberFree = function (cbf) {
    var self = this;
    self.api.getMemberInfo(function () {
        cbf(self.api.connect.accountType === 'FREE');
    });
};
waitForFreeAccnt.prototype.testMemberXTimes = function(maxAttempts, callback) {
  var attempts = 0;
  var self = this;
  (function attempt() {
    self.isMemberFree(function(free) {
      if (free)
        callback(true);
      else if (++attempts < maxAttempts)
        setTimeout(attempt, 1000);
      else
        callback(false);
    });
  )();
};
this.testMemberXTimes(10, function(isFree) {
  // the next part of your code here, or called from here
  // because at this point we know we've tested up to
  // ten times and isFree tells us the result
});

Note that the way I coded getMemberInfo() it is basically doing the same thing yours was, but instead of returning a boolean it is calling the callback function and passing the same boolean value that you were returning. (I've removed the console.log()s to make the code shorter.)

Note also that you could structure the above to use promises, but the end result will be the same.

Upvotes: 2

Samuel Toh
Samuel Toh

Reputation: 19238

I don't have my work laptop today because it is Sunday, I'm coding this on sublime. Apologise if the syntax is a bit off.

To solve your problem I would recommend changing isMemberFree() to take in a callback function. This is because isMemberFree is async, and you will need a way to report the result after it has done the work.

Then change test function to use setTimeout API to wait a second. Wrap the function call for isMemberFree() to be in a nested function and call it recursively, that way you'll have synchronize control over the async calls.

Look at the coding example:

waitForFreeAccnt.prototype.isMemberFree = function (done) {
    var self = this;

    self.api.getMemberInfo(function () {
        var accType = self.api.connect.accountType;
        console.log(accType);
        if (accType === 'FREE') {
            console.log('it is free');
            return done(null, true);
        } else {
            console.log('it is not free');
            return done(null, false);
        }
    });
};


var test = function () {

    var testMembership = function(waitAttempt, isFree) {
        if (isFree) { 
            return; 
        }
        else if (waitAttempt > 10) {
            // wait exceeded, do something.
            return;
        }
        setTimeout(function() {
            self.isMemberFree(function(err, isFree) {
                testMembership(waitAttempt+=1, isFree);
            });
        }, /*total milliseconds in 1sec=*/1000);
    }

    testMembership(/*WaitAttempts=*/0, /*isFree=*/false);
};

What the above code does is that, presumably something has already been done to the member's account and now test function is called. So it waits for 1 second, then call isMemberFree function, this happens recursively until either isMemberFree() returns true OR the 10 seconds wait has been exceeded.

Upvotes: 1

Related Questions