0

I'm having problem with multiple async calls. I have three tasks i want to apply. First i get some json data from first request. Then when that request has finished I make multiple request to getMovie trailers from another service and merge that data to every object in the first response. When all that has finished then i want to write that data to json file. Simple enough but I'm failing at promises.

Here's my code

    MovieService.getContent(config.url + '?key=' + config.moviekey).then(function(data) {
    var movies = JSON.parse(data);
    var cnt = 0;
    var len = movies.length;
    var results = [];

    for (var i = 0; i < movies.length; i++) {
        var imdbid = movies[i].ids.imdb;

        (function (i, imdbid) {
            MovieService.getContent(config.themoviedburl + 'tt' + imdbid + '/videos?api_key=' + config.themoviedbkey).then(function (data) {
                len--;
                movies[i].trailers = JSON.parse(data);
                if (len === 0) { 
                    FileService.writeToJson(JSON.stringify(movies), config.showtimesfilepath);
                }
            });
        })(i, imdbid);
    };
});

Now this works but what if one of the request in the loop would fail. Then my counter is incorrect and I would write the data to file. Could someone please help me set up similar scenario using promises. And ohh yes another pease of my code is the MovieService witch make all the requests

jonjonson
  • 263
  • 2
  • 5
  • 16
  • ahh yeah and this is my service – jonjonson Dec 13 '15 at 17:12
  • var q = require('q'); var request = require('request'); var kvikmyndir = { getContent: function(url) { var deferred = q.defer(); request(url, function (error, response, body) { if (!error && response.statusCode == 200) { deferred.resolve(body); } else { deferred.reject(error); } }); return deferred.promise; } }; module.exports = kvikmyndir; – jonjonson Dec 13 '15 at 17:12

2 Answers2

2

A common way to sequence a series of operations on an array using promises is with .reduce() where each iteration adds onto a promise chain causing all the async operations to be sequenced:

// in sequence
MovieService.getContent(config.url + '?key=' + config.moviekey).then(function(data) {
    var movies = JSON.parse(data);
    return movies.reduce(function(p, movie, index) {
        return p.then(function() {
            var imdbid = movie.ids.imdb;
            return MovieService.getContent(config.themoviedburl + 'tt' + imdbid + '/videos?api_key=' + config.themoviedbkey).then(function (data) {
                movies[index].trailers = JSON.parse(data);
            }, function(err) {
            // handle the error here and decide what should be put into movies[index].trailers
            });
        });
    }, Promise.resolve()).then(function() {
        return FileService.writeToJson(JSON.stringify(movies), config.showtimesfilepath);
    });
});

Conceptually, what this does is call movies.reduce() and the starting value passed into .reduce() is a resolved promise. Then each iteration through .reduce() adds onto the promise chain with something like p = p.then(...). This causes all the operations to be sequenced, waiting for one to complete before invoking the next. Then, inside of that .then() handler, it returns the MovieService.getContent() promise so that this iteration will wait for the inner promise to complete too.


You can probably do these operations in parallel too without forcing them to be sequenced. You just need to know when they are all done and you need to keep all the data in order. That could be done like this:

// in parallel
MovieService.getContent(config.url + '?key=' + config.moviekey).then(function(data) {
    var movies = JSON.parse(data);
    var promises = [];
    movies.forEach(function(movie, index) {
        var imdbid = movie.ids.imdb;
        promises.push(MovieService.getContent(config.themoviedburl + 'tt' + imdbid + '/videos?api_key=' + config.themoviedbkey).then(function (data) {
            movies[index].trailers = JSON.parse(data);
        }, function(err) {
            // handle the error here and decide what should be put into movies[index].trailers
        }));
    });
    Promise.all(promises).then(function() {
        return FileService.writeToJson(JSON.stringify(movies), config.showtimesfilepath);
    });
});

Or, using the Bluebird's promise library's helpful Promise.map(), here's a shorter parallel version

// use Bluebird's Promise.map() to run in parallel
MovieService.getContent(config.url + '?key=' + config.moviekey).then(function(data) {
    var movies = JSON.parse(data);
    Promise.map(movies, function(movie, index) {
        var imdbid = movie.ids.imdb;
        return MovieService.getContent(config.themoviedburl + 'tt' + imdbid + '/videos?api_key=' + config.themoviedbkey).then(function (data) {
            movies[index].trailers = JSON.parse(data);
        }, function(err) {
            // handle the error here and decide what should be put into movies[index].trailers
        });
    }).then(function() {
        return FileService.writeToJson(JSON.stringify(movies), config.showtimesfilepath);
    });
});

If you want the process to continue even if any given request fails, then you have to describe what you want to happen in that case and you can accomplish that by handling an error on .getContent() so that it always returns a resolved promise.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thx alot I used the middle approach – jonjonson Dec 13 '15 at 18:18
  • But what if one async response fails or many, but i still need to write to file reguardless of the failiure. What would be the best aproach to handle that? @jfriend00 – jonjonson Dec 13 '15 at 18:22
  • @jonjonson - the OP did not describe exactly what they wanted to do when there was an error in one of the requests. As my answer says, they can provide an error handler on `.getContent()` and provide whatever behavior they want there as providing an error handler there will "handle" the error there and keep it from propagating up. I've added that specific info to my code examples too. – jfriend00 Dec 13 '15 at 18:23
0

Instead of chaining the promises, you can just run the promises asynchronously, and when all promises are resolved whether or not any one of them fails, you can write to JSON using your FileService. This is done by using Promise.all().

For example,

MovieService.getContent(config.url + '?key=' + config.moviekey).then(function (data) {
  var movies = JSON.parse(data);
  var movieDataPromises = [];

  movies.forEach(function (movie) {
    var imdbid = movie.ids.imdb;

    movieDataPromises.push(
      MovieService.getContent(config.themoviedburl + 'tt' + imdbid + '/videos?api_key=' + config.themoviedbkey).then(function (trailerData) {
        movie.trailers = JSON.parse(trailerData);
      }).catch(function () {});
    );
  });

  Promise.all(movieDataPromises).then(function () {
    FileService.writeToJson(JSON.stringify(movies), config.showtimesfilepath);
  });
});

The reason why there is a catch with an empty callback body when retrieving the trailer info is because we want to prevent the Promise.all() from executing its fail-fast behavior.

EDIT: avoid using the promise constructor antipattern.

Community
  • 1
  • 1
boombox
  • 2,396
  • 2
  • 11
  • 15
  • Avoid the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572)! – Bergi Dec 13 '15 at 17:44
  • @Bergi - If there is any reject in `getContent()` when retrieving the trailer info for the movies, the `Promise.all()` will immediately reject with the rejected value whilst discarding all other promises. The antipattern in this case avoids this from happening by resolving even when there is a rejection, allowing the FileService.writeToJson() to execute in the Promise.all(). – boombox Dec 13 '15 at 18:07
  • That's what `return ….catch(function(){ /*ignore*/ })` would do as well, no need to use the antipattern. Alternatively look [here](http://stackoverflow.com/a/31424853/1048572) – Bergi Dec 13 '15 at 18:10
  • 1
    @Bergi - ahh yes! you are correct. Thank you. :) I have corrected my answer. – boombox Dec 13 '15 at 18:15