2

With the request library, is there a way to use promises to simplify this callback?

  var context = {};

  request.get({
    url: someURL,
  }, function(err, response, body) {

    context.one = JSON.parse(body);

    request.get({
      url: anotherURL,
    }, function(err, response, body) {
      context.two = JSON.parse(body);

      // render page
      res.render('pages/myPage');
    });
  });
cusejuice
  • 10,285
  • 26
  • 90
  • 145
  • 1
    Where do `someURL` and `anotherURL` come from, does the second request actually depend on the first one? Also, where is `context` used? And why don't you handle any errors? – Bergi Sep 08 '15 at 18:16

5 Answers5

5

Here's a solution using the Bluebird promises library. This serializes the two requests and accumulates the results in the context object and rolls up error handling all to one place:

var Promise = require("bluebird");
var request = Promise.promisifyAll(require("request"), {multiArgs: true});

var context = {};
request.getAsync(someURL).spread(function(response, body) {
    context.one = JSON.parse(body);
    return request.getAsync(anotherURL);
}).spread(response, body)
    context.two = JSON.parse(body);
    // render page
    res.render('pages/myPage');
}).catch(function(err) {
    // error here
});

And, if you have multiple URLs, you can use some of Bluebirds other features like Promise.map() to iterate an array of URLs:

var Promise = require("bluebird");
var request = Promise.promisifyAll(require("request"), {multiArgs: true});

var urlList = ["url1", "url2", "url3"];
Promise.map(urlList, function(url) {
    return request.getAsync(url).spread(function(response,body) {
        return [JSON.parse(body),url];
    });
}).then(function(results) {
     // results is an array of all the parsed bodies in order
}).catch(function(err) {
     // handle error here
});

Or, you could create a helper function to do this for you:

// pass an array of URLs
function getBodies(array) {
    return Promise.map(urlList, function(url) {
        return request.getAsync(url).spread(function(response.body) {
            return JSON.parse(body);
        });
    });
});

// sample usage of helper function
getBodies(["url1", "url2", "url3"]).then(function(results) {
    // process results array here
}).catch(function(err) {
    // process error here
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Added a couple more examples. – jfriend00 Sep 08 '15 at 21:10
  • @userpassword - I had to fix up your edit. I'm not sure why you changed the bluebird library in my answer from `Promise` which it is declared to be in my code to `bluebird`. I did accept your addition of `multiArgs` which is a change Bluebird made in 3.0. – jfriend00 Jan 09 '17 at 19:13
3

Here is how I would implement chained Promises.

var request = require("request");

var someURL = 'http://ip.jsontest.com/';
var anotherURL = 'http://ip.jsontest.com/';

function combinePromises(context){
  return Promise.all(
    [someURL, anotherURL].map((url, i)=> {

      return new Promise(function(resolve, reject){

        try{

          request.get({
            url: url,
          }, function(err, response, body) {

            if(err){
              reject(err);
            }else{
              context[i+1] = JSON.parse(body);
              resolve(1); //you can send back anything you want here
            }

          });

        }catch(error){
          reject(error);
        }

      });

    })
  );
}

var context = {"1": "", "2": ""};
combinePromises(context)
.then(function(response){

  console.log(context);
  //render page
  res.render('pages/myPage');
  
}, function(error){
  //do something with error here
});
1

Doing this with native Promises. It's good to understand the guts.

This here is known as the "Promise Constructor Antipattern" as pointed out by @Bergi in the comments. Don't do this. Check out the better method below.

var contextA = new Promise(function(resolve, reject) {
  request('http://someurl.com', function(err, response, body) {
    if(err) reject(err);
    else {
      resolve(body.toJSON());
    }
  });
});

var contextB = new Promise(function(resolve, reject) {
  request('http://contextB.com', function(err, response, contextB) {
    if(err) reject(err);
    else {
      contextA.then(function(contextA) {
         res.render('page', contextA, contextB);
      });
    }
  });
});

The nifty trick here, and I think by using raw promises you come to appreciate this, is that contextA resolves once and then we have access to it's resolved result. This is, we never make the above request to someurl.com, but still have access to contextA's JSON.

So I can conceivable create a contextC and reuse the JSON without having to make another request. Promises always only resolve once. You would have to take that anonymous executor function out and put it in a new Promise to refresh that data.

Bonus note:

This executes contextA and contextB in parallel, but will do the final computation that needs both contexts when both A & B are resolved.

Here's my new stab at this.

The main problem with the above solution is none of the promises are reusable and they are not chained which is a key feature of Promises.

However, I still recommend promisifying your request library yourself and abstaining from adding another dependency to your project. Another benefit of promisifying yourself is you can write your own rejection logic. This is important if you're working with a particular API that sends error messages in the body. Let's take a look:

//Function that returns a new Promise. Beats out constructor anti-pattern.
const asyncReq = function(options) { 
 return new Promise(function (resolve, reject) {
  request(options, function(err, response, body) {
   //Rejected promises can be dealt with in a `catch` block.
   if(err) {
    return reject(err);
   } 
   //custom error handling logic for your application.
   else if (hasError(body)) {
    return reject(toError(body));
   }
   // typically I just `resolve` `res` since it contains `body`.
   return resolve(res);
  }
 });
};

asyncReq(urlA)
.then(function(resA) {
  //Promise.all is the preferred method for managing nested context.
  return Promise.all([resA, asyncReq(urlB)]);
})
.then(function(resAB) {
  return render('page', resAB[0], resAB[1]);
})
.catch(function(e) {
  console.err(e);
});
Breedly
  • 12,838
  • 13
  • 59
  • 83
  • Avoid the [promise constructor antipattern](http://stackoverflow.com/q/23803743/1048572) – Bergi Sep 12 '15 at 20:14
1

You can use the request-promise library to do this. In your case, you could have something like this, where you chain your requests.

request
    .get({ url: someURL })
    .then(body => {
        context.one = JSON.parse(body);

        // Resolves the promise
        return request.get({ url: anotherURL });
    })
    .then(body => {
        context.two = JSON.parse(body);
        res.render('pages/myPage');
    })
    .catch(e => {
        //Catch errors
        console.log('Error:', e);
    });
ethanc
  • 137
  • 2
  • 12
0

By far the easiest is to use request-promise library. You can also use use a promise library like bluebird and use its promisify functions to convert the request callback API to a promise API, though you may need to write your own promisify function as request does not use the standard callback semantics. Lastly, you can just make your own promise wrapper, using either native promises or bluebird.

If you're starting fresh, just use request-promise. If you're refactoring existing code, I would just write a simple wrapper for request using bluebird's spread function.

Yuri Zarubin
  • 11,439
  • 4
  • 30
  • 33