1

So I have a situation where I need to build an Object, then inject some items into that Object, and then make an API call and also inject the API's response into that Object. It's a complex Object so we'll just assume that's alright, but anyway, I think that I need to do these 3 things synchronously and using array.forEach means they run asynchronously.

I hope my example is simple enough to understand, but basically I do 3 things:

  • Create an empty Array of Classes/Classrooms
  • Loop through an Array of Class IDs and create an Object for each Class
  • Loop through an Array of Students and push them into a students Array inside the Class Object
  • Loop through an Array of Options and push them into an options array inside the Class Object

Finally, I have something that could look like this for each Class:

{
  class_id: "abc123",
  students: [{}, {}, {}],
  options: [{}, {}, {}]
}

And finally, here is my code:

// Create Array of Class Objects
var classes = [];

function processArray(array, func) {
  return $q(function(resolve, reject) {
    array.forEach(function(item, index) {
      func(item);
      if (index === (array.length - 1)) resolve();
    })
  })
}

// Create Courier Objects
processArray(classIds, function(id) {
  classes.push({class_id: id, students: [], options: []});
}).then(function(response) {
  // Inject Students into each Class
  processArray(students, function(students) {
    _.find(classes, {'class_id': student.class_id}).students.push(student);
  }).then(function(response) {
    // Inject classOptions into each Class
    processArray(classOptions, function(classOption) {
      _.find(classes, {'class_id': classOption.class_id}).classOptions.push(classOption);
    }).then(function(response) {
      // Print the classes
      console.log(classes);
    })
  })
});

I've create a function which does it synchronously but I'd like to know if anyone can think of a much, much cleaner and more efficient way of doing the above. It seems extremely hacky, and maybe I don't even need to do it synchronously if I arranged my functions correctly.

germainelol
  • 3,231
  • 15
  • 46
  • 82
  • 2
    Just chain the promises together. If they need to be synchronous, then what you have works fine. – ryanyuyu Mar 31 '16 at 15:48
  • @ryanyuyu Am I right in assuming that chaining the promises would result in something like `function1().then(function2().then(function3().then('done')))`? – germainelol Mar 31 '16 at 15:51
  • Promises return promises when [`.then()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) resolves, so you can just do `promise1.then(func1).then(func2).then(func3)` – ryanyuyu Mar 31 '16 at 15:54
  • @ryanyuyu Makes sense, so I'd basically be removing the `function(response) {}` part from my `.then()`s above in this case and just doing `processArray().then(processArray())`. Would save a bit of code. – germainelol Mar 31 '16 at 15:58
  • Yeah and make it more readable. I don't really know anything simpler than this for chaining promises. – ryanyuyu Mar 31 '16 at 16:11

2 Answers2

1

you are using nested promises. These suck. One major advantage of promises is to avoid the 'callback nightmare' of deep nesting.

Use this syntax ...

promise1.then(function(response1) {
  // do something with response 1 (and possibly create promise2 here based on response1 if required)
  return promise2
}).then(function(response2) {
  // do something with response 2
  return promise3
}).then(function(response3) {
  // do something with response 3
  // do something with promise 3???
}).catch(function(errorResponse) {
    // will trigger this on a failure in any of the above blocks
    // do something with error Response
});
user229044
  • 232,980
  • 40
  • 330
  • 338
danday74
  • 52,471
  • 49
  • 232
  • 283
1

Working with Promise Based APIs that Return Arrays

The processArray function returns a promise that resolves to null.

//WRONG
//Returns a promise that resolves to null
//
function processArray(array, func) {
  return $q(function(resolve, reject) {
    array.forEach(function(item, index) {
      func(item);
      if (index === (array.length - 1)) resolve();
    })
  })
}

To return a promise that resolves to an array, use $q.all.

//RIGHT
//Returns a promise that resolves to an array
//
function processArray(array, func) {
    var promiseArray = [];
    array.forEach(function(item, index) {
        promiseArray.push($q.when(func(item));
    });
    return $q.all(promiseArray);
}

In either case whether func(item) returns either a value or a promise, $q.when will return a promise.

Be aware that $q.all is not resilient. It will resolve fulfilled with an array of values or it will resolve rejected with the first error.

processArray(array, func)
    .then( function onFulfilled(dataList) {
        //resolves with an array
        $scope.dataList = dataList;
    }).catch( function onRejected(firstError) {
        console.log(firstError);
    });

For more information, see AngularJS $q Service API Reference.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Thanks for your answer. Am I right in assuming that the `.catch()` can also come after several chained `.then()`s as well? To make sure I don't have to do `.then().catch().then().catch()....` – germainelol Apr 01 '16 at 07:43
  • Yes, when a promise resolves rejected, all the subsequent `.then` functions in the chain get skipped until the next `.catch`. Just like `try` `catch` blocks in synchronous code. For more information on chaining promises, see [Angular execution order with `$q`](http://stackoverflow.com/questions/34324153/angular-execution-order-with-q). – georgeawg Apr 01 '16 at 07:58