5

I am new to Promises and I was wondering what the best practice is to keep variables while going down the chain?

Connecting to MongoDB via Promise is pretty straight forward:

connectToMongoDB(data).done(function(db) {

    var collection = db.collection('test_inserts');
    // do more stuff here

});

But what happens if I have to connect to two different databases?

connectToMongoDB1(data1).then(function(db1) {

    return connectToMongoDB2(data2);

}).done(function(db2) {

    var collection = db1.collection('test_inserts');
    // ERROR: db1 is undefined

});

That error makes perfect sense. But how do I forward db1 without altering my connectToMongoDB2() function, because I want to keep connectToMongoDB2() and all my promises in general very generic?

I mean, I could wrap an object around that stores all the relevant stuff, but that looks kind of hacky:

var relevantStuff = {};

connectToMongoDB1(data1).then(function(db1) {

    relevantStuff.db1 = db1;
    return connectToMongoDB2(data2);

}).done(function(db2) {

    var collection = relevantStuff.db1.collection('test_inserts');
    // do more stuff here

});

What is the best practice?

Amberlamps
  • 39,180
  • 5
  • 43
  • 53
  • Nesting callbacks. No, really. Everything else (except generators) will look hacky. – Bergi Jul 07 '14 at 17:05
  • Check out `Promise.using` in Bluebird - this is a great question and this gets a __lot__ tricker when you get to disposing multiple resources in exceptional conditions. – Benjamin Gruenbaum Jul 07 '14 at 17:21
  • Also, some promise libraries like Bluebird include methods that let you do this a lot more easily (like `.bind` for instance) - can we assume a specific promise library? – Benjamin Gruenbaum Jul 07 '14 at 17:21
  • @BenjaminGruenbaum: I am using Bluebird to build my promises. I have to read through its docs. Thanks for the hint! – Amberlamps Jul 07 '14 at 19:09

3 Answers3

12

Note: I'm using bluebird in this answer.

There are 3 ways to do what you want: closures, binding, and Promise.using.

Closure is the way @Sukima showed.

function promisedFunc() {
    var db;
    return getCollection().then(function(col) {
        db = col;
        return db.query(stuff);
    }).then(function() {
        return db.query(otherStuff);
    });
}

Binding: using Promise.bind, you can make this an object that will hold values.

function promisedFunc() {
    return getCollection().bind({}).then(function(col) {
        this.db = col;
        return this.db.query(stuff);
    }).then(function() {
        return this.db.query(otherStuff);
    });
}

Finally, the last way, introduced by bluebird v2, is using real resource management.

function promisedFunc() {
    return Promise.using(getDb(), function(db) {
        return db.query(stuff).then(function() {
            return db.query(otherStuff);
        });
    });
}

I'm going to explain the getDb method further down.


The last way provides another very interesting benefit: disposing resources. For example, you often have to call a close method for database resources. Promise.using lets you create disposers, running once the promises in it are resolved (or not).

To see why this is an advantage, let's review the 3 ways to do this.

Closure:

var db, close;
return getCollection().spread(function(col, done) {
    db = col;
    close = done;
    return db.query(stuff);
}).then(function() {
    return db.query(otherStuff);
}).finally(function() {
    close();
});

And yes, it means you have to write all this boilerplate every time you use the db connection. No other choice.

Now let's see the binding way:

return getCollection().bind({}).spread(function(col, done) {
    this.db = col;
    this.close = done;
    return this.db.query(stuff);
}).then(function() {
    return this.db.query(otherStuff);
}).finally(function() {
    this.close();
});

Now however, this can be modularized.

var db = {
    getDb: function() { return getCollection().bind({}); },
    close: function() { this.close(); }
};

return db.getDb().then(function() {
    return this.db.query(stuff);
}).then(function() {
    return this.db.query(otherStuff);
}).finally(db.close);

This is already a lot nicer! But we still have to think about using finally.

And then, the way introduced by bluebird, Promise.using. By declaring it this way:

function getDb() {
    var close;
    return getCollection().spread(function(db, done) {
        close = done;
        return db;
    }).disposer(function() {
        close();
    });
}

You can simply use it as seen before:

return Promise.using(getDb(), function(db) {
    return db.query(stuff).then(function() {
        return db.query(otherStuff);
    });
});

No need to think about finally, and no boilerplate.

Florian Margaine
  • 58,730
  • 15
  • 91
  • 116
  • The `Promise.using` way is really the most superior when you're connecting to multiple databases, in which case it will handle resource management for you (in case one DB fails). – Benjamin Gruenbaum Jul 07 '14 at 17:30
  • @BenjaminGruenbaum edited the answer to add a simple explaination why `Promise.using` is better, even with a single database. – Florian Margaine Jul 07 '14 at 17:36
  • Thanks Florian! The first two examples are trivial, but the last one leaves me scratching my head a bit. Gotta dig into this! – Amberlamps Jul 07 '14 at 19:15
2

Typically if your managing multiple return values (in your case) it is encapsulated in some form. Here are some popular options:

  • outer scoped variable
  • return an array of multiple values in each chained function
  • or use an object to represent the various states and return that object's instance with each chained function.

Outer scoped variable:

function promisedFunc() {
  var db;
  return getCollection()
    .then(function(db1) {
      db = db1;
      // Do stuff
      return stuff;
    })
    .then(function(db1) {
      // Do stuff
      return db.stuff;
    });
}

Returning multiple values:

function getConnection() {
  return getCollection()
    .then(function(db1) {
      // Do stuff
      return [stuff, db1];
    })
    .then(function(args) {
      var stuff = args[0];
      var db = args[1];
      // Do stuff
      return [db.moreStuff, db];
    });
}

Objects:

function getConnection() {
  function DB(db1, stuff) {
    if (db1 instanceof DB) { return new DB(db1.db1, stuff); }
    this.db1 = db1;
    this.stuff = stuff;
  }
  // Add to prototype etc.
  return getCollection()
    .then(function(db1) {
      // Do stuff
      return new DB(db1, stuff);
    })
    .then(function(db) {
      // Do stuff
      return new DB(db, stuff);
    });
}
Sukima
  • 9,965
  • 3
  • 46
  • 60
1

Here is what I'd do, in the generic case of needing scope access. I'd use the promises as the proxies they are. Other answers overlooked this technique I personally use often, and I think it's an alternative worth considering.

Assuming Bluebird:

var db1 = connectToMongoDB(data1);
var db2 = connectToMongoDB(data2); //  if needs sequencing, do db1.then...

Promise.join(db1,db2,function(db1,db2){
      // access both connections here, here both promises are available
      // in native promises this is Promise.all([db1,db2]).then(...)
});

No nesting involved when you don't need to wait, everything looks serial, if you actually needed waiting for DB1 and couldn't connect in parallel:

var db1 = connectToMongoDB(data1);
var db2 = db1.then(function(data){ data2 = data[0]; connectToMongoDB(data2);});

As for resource management - check out Florian's good answer - although in this case MongoDB connections are built to be persistent, and you should open/close them exacty once (usually) in your application.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504