Using Q/promises vs callbacks

Problem

I'm using the Q library in nodejs and haven't worked too much with promises in the past, but I have semi complex logic that requires lots of nesting and thought Q would be a good solution, however I'm finding that it seems to be almost the same as just "callback hell".

Basically I have say 5 methods, all which require data from the previous or one of the previous. Here's an example:

We start with some binary data that has a sha1 hash generated based on the binary.

var data = {
    hash : "XXX"
  , binary: ''
}

First we want to see if we already have this, using this method:

findItemByHash(hash)

If we don't have it, we need to save it, using:

saveItem(hash)

Now we need to associate this to a user, but not only the results of the save. There's now a much larger hierarchy that we associate, so we need to get that first, doing:

getItemHierarchy(item_id), we use the item_id returned from our previous saveItem

Now, we can "copy" these results to a user:

saveUserHierarchy(hierarchy)

Now we're done, however, this assumes the item didn't exist yet. So we need to handle a case where the item did exist. This would be:

We need to check if the user may aleady have this:

getUserItemByItemId(item_id) - item_id was returned from findItemByHash

If it exists, we're done.

If it doesn't:

getItemHierarchy(item_id)

Then

saveUserHierarchy(hierarchy)

Ok, so right now we have callbacks that do these checks, which is fine. But we need to handle errors in each case along the way. That's fine too, just adds to the mess. Really, if any part of the flow throws an error or rejects then it can stop and just handle it in a single place.

Now with Q, we could do something like this:

findItemByHash(hash).then(function(res) {

    if (!res) {

     return saveItem(hash).then(function(item) {
        return getItemHierarchy(item.id).then(function(hierarchy) {
            return saveUserHierarchy(hierarchy);
        });
     })

    } else {

      return getUserItemByItemId(res.id).then(function(user_item) {

         if (user_item) {
            return user_item;
         } 

        return getItemHierarchy(res.id).then(function(hierarchy) {
            return saveUserHierarchy(hierarchy);
        });

      });

    }
})
//I think this will only handle the reject for findItemByHash?
.fail(function(err) {
   console.log(err);
})
.done();

So, I guess my question is this. Are there better ways to handle this in Q?

Thanks!

Problem courtesy of: dzm

Solution

One of the reasons why I love promises is how easy it is to handle errors. In your case, if any one of those promises fail, it will be caught at the fail clause you have defined. You can specify more fail clauses if you want to handle them on the spot, but it isn't required.

As a quick example, sometimes I want to handle errors and return something else instead of passing along the error. I'll do something like this:

function awesomeFunction() {
    var fooPromise = getFoo().then(function() {
        return 'foo';
    }).fail(function(reason) {
        // handle the error HERE, return the string 'bar'
        return 'bar';
    });

    return fooPromise;
}

awesomeFunction().then(function(result) {
    // `result` will either be "foo" or "bar" depending on if the `getFoo()`
    // call was successful or not inside of `awesomeFunction()`
})
.fail(function(reason) {
    // This will never be called even if the `getFoo()` function fails
    // because we've handled it above.
});

Now as for your question on getting out of "return hell" - as long as the next function doesn't require information about the previous one, you can chain .then clauses instead of nesting them:

doThis().then(function(foo) {
    return thenThis(foo.id).then(function(bar) {
        // `thenThat()` doesn't need to know anything about the variable
        // `foo` - it only cares about `bar` meaning we can unnest it.
        return thenThat(bar.id);
    });
});

// same as the above
doThis().then(function(foo) {
    return thenThis(foo.id);
}).then(function(bar) {
    return thenThat(bar.id);
});

To reduce it further, make functions that combine duplicate promise combinations and we're left with:

function getItemHierarchyAndSave(item) {
    return getItemHierarchy(item.id).then(function(hierarchy) {
        return saveUserHierarchy(hierarchy);
    });
}

findItemByHash(hash).then(function(resItem) {
    if (!resItem) {
        return saveItem(hash).then(function(savedItem) {
            return getItemHierarchyAndSave(savedItem);
        });
    }

    return getUserItemByItemId(resItem.id).then(function(userItem) {
        return userItem || getItemHierarchyAndSave(resItem);
    });
})
.fail(function(err) { console.log(err); })
.done();

Disclaimer: I don't use Q promises, I perfer when promises primarily for the extra goodies it comes with, but the principles are the same.

Solution courtesy of: Trevor Senior

Discussion

There is currently no discussion for this recipe.

This recipe can be found in it's original form on Stack Over Flow.