7

From what I understand one of the main selling points for Promises is the ability to write flat code (or, flatter than callback hell).

Though it seems that in many cases we need to nest promises, in order to use closure. For example (from q's docs, though I use Bluebird):

function authenticate() {
    return getUsername()
        .then(function (username) {
            return getUser(username);
        })
        // chained because we will not need the user name in the next event
        .then(function (user) {
            return getPassword()
                // nested because we need both user and password next
                .then(function (password) {
                    if (user.passwordHash !== hash(password)) {
                        throw new Error("Can't authenticate");
                    }
                });
        });
}

Is there a cleaner way to do this, without nesting?

EDIT: I've managed to clean up this specific example using .all, but there are more complex cases where I don't think it can be done:

function authenticate() {
    return Promise.all([
        getUsername().then(getUser),
        getPassword()
    ]).spread(function (user, password) {
        if (user.passwordHash !== hash(password)) {
            throw new Error('Can\'t authenticate');
        }
    });
}
  • A little cleaner: http://jsfiddle.net/jnqx4uqw/ – dfsq Oct 05 '14 at 16:58
  • Yes, but it doesn't get rid of the nesting –  Oct 05 '14 at 17:09
  • 3
    Sometimes promises remove the need for nesting and sometimes they do not. It really depends upon the details of the situation. If you're sequencing operations, you don't have to nest just to share results. You can assign values to something available in a common parent closure and do it that way too and then not have to nest. – jfriend00 Oct 05 '14 at 17:23
  • No. Eliminating callback hell as it is called is only one of the reason to use Promises, IMO the less important. If you think about it the callbacks are still present, they are just wrapped in some promise library. – webduvet Oct 05 '14 at 19:01

1 Answers1

10

Yes, you can always flatten a promise chain with Promise.all (shorthand through Promise.join in Bluebird) by using promises for the proxies they are. After all - promises abstract values, you can always unwrap a promise as late as you want and have other variables depend on it.

Whether or not it's more readable is another debate:

foo().then(function(a){
     return bar(a).then(function(b){
          return g(a,b); // "needed" to nest because I wanted `a`
     });
});

Can be written as:

var a = foo();
var b = a.then(bar);
Promise.join(a, b, function(a,b){
    return g(a, b); // alternatively, res could have been `Promise.join(a,b, g)`
});

So generally - you can always avoid nesting but a lot of time you might not want to.

In your case, this can even be:

function authenticate() {
    var userPass = Promise.all([ getUsername().then(getUser), getPassword()]);
    var passHash = userPass.get(0).get("passwordHash");
    var newHash = userPass.get(1).then(hash);     
    var equal = Promise.join(userHash, newHash, function(a, b){ return a !==b });
    return equal.then(function(val){ if(!val) throw new Error("..."); });
}

Flatter? Sure. Better? That's a whole other question. If you have a nested for loop, you might want to keep it a nested for loop and nest rather than hack around that option and use a single loop.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 2
    Good stuff, your a,b,c example was exactly what I was looking for. On a side note, I don't like using .join because even though it saves a few keystrokes, I feel it's less readable (and I'm pretty sure it's not in the standard) –  Oct 06 '14 at 06:59
  • That's fine, I'm just used to it because the pattern is extremely common, it saves me the `.spread` and it's also a bit faster. – Benjamin Gruenbaum Oct 06 '14 at 07:02