3

I'm having a weird situation where I want to break a for loop after I have received the result of an Rx Promise and done some checks. What I have is the following:

function getDrift(groups) {
    var drift = {};
    groups.forEach(function(group) {
        if(group.type === 'something') {
            for(var i = 0; i < group.entries.length; i++) {
                fetchEntry(group.entries[i].id)
                 .then(function(entry) {
                     if(entry.type === 'someType'){
                         drift[entry._id] = getCoordinates(entry);
                         // break;
                     }
                 });
            }
        }
    });
    return drift;
}

where fetchEntry is returning a Promise of a mongodb document based on an id. If the if check is satisfied, I want to break the loop on the current group.entries and continue on to the next group.

Is that possible?

Thanks

EDIT: As requested, the groups object looks like this:

[
    {
        type: 'writing',
        entries: [{id: "someId", name: "someName"}, {id: "someId2", name: "someName2"}]
    },
    {
        type: 'reading',
        entries: [{id: "someId3", name: "someName3"}, {id: "someId4", name: "someName4"}]
    }
]

SOLUTION: I ended up using @MikeC 's suggestion with recursion and a callback to return the needed value. Thank you all!

XeniaSis
  • 2,192
  • 5
  • 24
  • 39
  • The problem you've got here is the request is (presumably) asynchronous, while the loop is synchronous. Thus, the former cannot break the latter as, by the time the callback fires, the loop has long finished. – Mitya Jan 28 '16 at 16:31
  • See http://stackoverflow.com/questions/33587861/how-to-break-out-of-a-serial-loop-when-using-promises/33590119#33590119 – jib Jan 29 '16 at 01:37

4 Answers4

13

That isn't possible because Promises are asynchronous which means then won't execute until all other synchronous code completes.

If you don't want to process all of them based on some condition, I would suggest creating a function which you call if you want to continue.

(function process(index) {
  if (index >= group.entries.length) {
    return;
  }
  fetchEntry(group.entries[index])
    .then(function(entry) {
      if(entry.type === 'someType'){
        drift[entry._id] = getCoordinates(entry);
        // don't call the function again
      } else {
        process(index + 1);
      }
    });
})(0);
Mike Cluck
  • 31,869
  • 13
  • 80
  • 91
4

The function you pass to then() is not called inside the for loop. It is called (long) after the for loop has finished. This is the essence of the asynchronous programming model.

You will need to reorganize your code so that you don't use a for loop. Instead you need to initiate the next fetch within the callback, or don't initiate it as appropriate.

PS. You can't return an object populated by the callbacks, either, for the same reason: your function will return the empty object /before/ the callbacks are called


Edit: demonstration, but code untested:

function getDrift(groups) {
    var promise = ...;

    var drift = {};
    groups.forEach(function(group) {
        if(group.type === 'something') {
            var i = 0;

            var processEntry = (function(entry) {
                     if(entry.type === 'someType'){
                         drift[entry._id] = getCoordinates(entry);

                         // We are finished, so complete our promise with
                         // the collected data
                         promise.success(drift);
                         return;
                     }

                     // increment our position in the array
                     i += 1;

                     // check to see if we are at the end of the array
                     if (i >= group.entries.length)
                        { return; }

                     // now fetch the next entry from the array
                    fetchEntry(group.entries[i].id)
                            .then(processEntry);
                 });

            // fetch the first entry
            fetchEntry(group.entries[i].id)
                 .then(processEntry);

            } // end if

    }); // end forEach()

    return promise;
}
dsh
  • 12,037
  • 3
  • 33
  • 51
  • I figured that, but I'm not really sure how to do it. Thanks anyway! – XeniaSis Jan 28 '16 at 16:35
  • I updated my answer with some code, but it needs some finishing to fill in parts of the promise handling: create the promise, and pass it the data when successful. It shows how you need to encapsulate the behaviour in a function and to proceed through the array asynchronously in the callback instead of synchronously in a loop. – dsh Jan 28 '16 at 16:49
1

This is possible without recursion, but not particularly simple.

You can use a combination of :

  • replacing the forEach() with Array#map() to map groups to an array of promises,
  • replacing the original for loop with Array#reduce() to build a .then() chain, which is "breakable" by sending it down its error path.

Simplest thing by far is to stick with drift as an outer variable as per the code in the question. Delivering the data via the promise chain is also possible but the code would be more involved.

function getDrift(groups) {
    var drift = {};
    // Map groups to an array of promises.
    var promises = groups.map(function(group) {
        if(group.type === 'something') {
            // Here, replace the original `for` loop with `group.entries.reduce()` to build a .then() chain.
            return group.entries.reduce(function(p, e) {
                return p.then(function() {
                    return fetchEntry(e.id).then(function(entry) {
                        if(entry.type === 'someType') {
                            drift[entry._id] = getCoordinates(entry); // Yay!
                            throw false; // Throw almost anything here to skip the rest of the .then() chain.
                        } else {
                            return true; // Return almost anything here to carry on down the chain.
                        }
                    });
                });
            }, Promise.resolve()) // Resolved starter promise for the reduction.
            .catch(function() { // If there was a match, we end up on the error path, and need to convert to success.
                return true; // Return almost anything here.
            });
        } else {
            return true; // Return almost anything here to make a positive entry on the `promises` array.
        }
    });
    return Promise.all(promises) // Aggregate the promises.
    .then(function() {
        return drift; // and deliver the populated `drift` to the calling function
    });
}

As getDrift() returns a promise, drift only becomes available to the caller in a .then() callback :

getDrift(groups).then(function(drift) {
    //do something with drift
});
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
0

If the if check is satisfied, I want to break the loop on the current group.entries and continue on to the next group.

If interpret Question correctly, try using Promise.all() , Array.prototype.map()

function getDrift(groups) {
  var drift = {};
  var m = 0;
  return Promise.all(groups.map(function(p) {
    // if `p` contains `5` return `null`
    // else multiply item within `p` by `10`
    return p.indexOf(5) === -1 ? p.map(function(k) {
      drift[m] = k * 10;
      ++m
    }) : null
  })).then(function(n) {
    return drift
  }, function(err) {
    console.log(err)
  })

}
// array containing `5` will not be processed
var res = getDrift([[1,2,3], [4,5,6], [7,8,9]])
.then(function(data) {
  console.log(data)
})

Applying pattern above to js at Question

function getDrift(groups) {
  var drift = {};
  return Promise.all(groups.map(function(group) {
    return group.type === "something" 
    ? Promise.all(group.entries.map(function(g, index) {
      return fetchEntry(g.id).then(function(entry) {
         if (entry.type === "someType") {
            drift[entry._id] = getCoordinates(entry);
            return Promise.reject(entry.type);
         }
      })
    })) 
    : null
  })).then(function() {
    return drift
  }, function(err) {
    console.log(err)
  })

}
// array containing `5` will not be processed
var res = getDrift(groups)
.then(function(data) {
  console.log(data)
}, function(err) {
  console.log(err)
})
guest271314
  • 1
  • 15
  • 104
  • 177