1

Let's say I have 3 files.

index.js makes a call to the backend like this

$.post('/test/', data, function(response) {
 //handle success here
})

routes.js handles the route like this

app.post('/test/', function(req, res){
   item.getItems(function(response){
     res.json(response);
   });
 });

items.js is the model which accesses the database and makes a POST request for each item

function getItems(callback) {
  database.query('SELECT * from items', function(result){
    result.forEach(function(item){
        request.post('/api/', item, function(req, res) {
          //finished posting item
        });
    });   
  });
  //callback here doesnt wait for calls to finish
}

where/when should I call the callback passed to getItems() to handle a success/failure in index.js?

Jack Johnson
  • 1,327
  • 3
  • 12
  • 18

2 Answers2

2

Because your request.post() operations are async, you have to use some method of keeping track of when they are all done and then you can call your callback. There are multiple ways of doing that. I'll outline a few:

Manually Keeping Track of Count of Request Operations

function getItems(callback) {
  database.query('SELECT * from items', function(result){
    var remaining = result.length;
    result.forEach(function(item){
        request.post('/api/', item, function(err, res) {
          --remaining;
          //finished posting item
          if (remaining === 0) {
              callback();
          }
        });
    });   
  });
}

The main problem with doing this manually is that propagating error in nested async operations is difficult when you get to actually figuring out how you're going to handle errors. This is much easier in the other methods shown here.

Using Promises

// load Bluebird promise library
var Promise = require('bluebird');

// promisify async operations
Promise.promisifyAll(request);

function queryAsync(query) {
    return new Promise(function(resolve, reject) {
        // this needs proper error handling from the database query
        database.query('SELECT * from items', function(result){
            resolve(result);
        });
    });
}

function getItems(callback) {
    return queryAsync('SELECT * from items').then(function(result) {
        return Promise.map(result, function(item) {
            return request.postAsync('/api/', item);
        });
    });
}

getItems.then(function(results) {
    // success here
}, function(err) {
    // error here
})
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I haven't tried yet, i'll try it out first thing tomorrow. Thanks. – Jack Johnson Oct 20 '15 at 23:11
  • @jfriend00, I'm curious to know how the `Promise.map` lines up with **// error here** portion of `getItems`... let's say you have 20 items for `postAsync`, where 15 succeed and 5 fail... will the 15 be in the `results` for success and the 5 be in the `err` for error? I have a high level understanding of what promises do, but I haven't seen an example with an array of promises yet. – incutonez Oct 21 '15 at 00:35
  • 1
    @incutonez - `Promise.map()` works like the ES6 `Promise.all()` in that regard. The bluebird doc is [here](http://bluebirdjs.com/docs/api/promise.map.html) where it says: **If any promise in the array is rejected, or any promise returned by the mapper function is rejected, the returned promise is rejected as well.** – jfriend00 Oct 21 '15 at 00:45
  • 1
    @incutonez - Life is a little more complicated if you want to know when all promises are done and you want all the results that succeeded and to know which ones failed. For that, you need to create an array of promises and use Bluebird's [`Promise.settle()`](https://github.com/petkaantonov/bluebird/blob/master/API.md#settle---promise) and then examine each result to see which ones succeeded and which ones failed. Here's a related question: http://stackoverflow.com/questions/25965977/what-to-use-instead-of-promise-all-when-you-want-all-results-regardless-of-any – jfriend00 Oct 21 '15 at 00:46
  • 1
    @incutonez - And, here's a plain JS implementation of a `.settle` type behavior: http://stackoverflow.com/questions/32468302/implementing-a-mix-of-promise-all-and-promise-settle/32469498#32469498. – jfriend00 Oct 21 '15 at 00:48
  • @jfriend00, oh wow, thanks, that's what I had thought. Looks like this stuff gets a lot more complicated than I thought... very interesting reads. – incutonez Oct 21 '15 at 01:27
  • @incutonez - I don't think of it as complicated. I just think of there are a lot of different options for managing the flow of control with multiple asynchronous operations and it takes several different tools to manage all the possibilities. But, it's really nice to have tools for doing so and to be doing it in a somewhat "standard" way too. – jfriend00 Oct 21 '15 at 02:41
  • To me, it sounds like the standard way needs to be reworked... sometimes having too many options is a problem, but such is the way in the life of JavaScript programming... a necessary evil. – incutonez Oct 21 '15 at 04:28
  • @incutonez - It's programming. There are always lots of different ways to do things. The good developers learn how to use the best or most appropriate tool/techniques for the job at hand. – jfriend00 Oct 21 '15 at 04:39
  • Agreed, but with all of the frameworks, libraries, design patterns, etc., it's easy for anyone to get lost in the noise of JS. – incutonez Oct 21 '15 at 05:11
  • @incutonez - I hear you. You start by learning "A" good way to do something and then over time you learn additional options so you can better choose the best tool for a job. Promises are a great tool to understand, particularly when managing the flow of control and error handling of multiple asynchronous operations. `Promise.all()` and `Promise.settle()` are a couple tools in the toolbox. – jfriend00 Oct 21 '15 at 05:27
1

It seems strange that you're making an API request in your server-side code, unless this is some sort of middle tier code that interacts with the API... but you're interacting with a database, so I'm still confused on why you can't just do a database insert, or have a bulk insert API call?

Anyway, if you must do it the way you're asking, I've done this in the past with a recursive method that trims down the result array... I really don't know if this is a good approach, so I'd like to hear any feedback. Something like this:

function recursiveResult(result, successfulResults, callback) {
  var item = result.shift();
  // if item is undefined, then we've hit the end of the array, so we'll call the original callback
  if (item !== undefined) {
    console.log(item, result);
    // do the POST in here, and in its callback, call recursiveResult(result, successfulResults, callback);
    successfulResults.push(item);
    return recursiveResult(result, successfulResults, callback);
  }
  // make sure callback is defined, otherwise, server will crash
  else if (callback) {
    return callback(successfulResults);
  }
  else {
    // log error... callback undefined
  }
}

function getItems(callback) {
  var successfulResults = [];
  var result = [1, 2, 3, 4];
  recursiveResult(result, successfulResults, callback);
}

console.log('starting');
getItems(function(finalResult) {
  console.log('done', finalResult);
});
incutonez
  • 3,241
  • 9
  • 43
  • 92
  • Thanks, the API request on the server side is just needed to update the items on another server. That looks like a good option albeit a bit complicated. If there's no other simpler solutions, I may just go with this. Another idea I had was to use lodash after function and call the callback there.. – Jack Johnson Oct 20 '15 at 03:00
  • Ah, ok, that's what I was figuring. Can you not make a bulk API call? If you've abstracted out your individual POST's logic, then it'd be very easy to make a bulk insert method. I agree that's it a bit complicated and at first, very confusing, but it seemed to get the job done for me. – incutonez Oct 20 '15 at 03:05
  • Can you explain what you mean by bulk API call? – Jack Johnson Oct 20 '15 at 03:14
  • By bulk API call, I mean your endpoint will consume an array of items, instead of a singular item... so you don't have to keep hitting the individual POST endpoint and worrying about where to place the callback. – incutonez Oct 20 '15 at 04:32
  • Ah I see what your saying. So just POST the whole itemsArray instead of single items right? Unfortunately I don't have access to how that endpoint handles the data so that won't be possible. – Jack Johnson Oct 20 '15 at 05:04
  • Yessir, that's exactly it. Hmm, ok, then unfortunately, you'll have to work with what you've got. – incutonez Oct 20 '15 at 14:48