JavaScript Promises – Gotchas and Anti Patterns

tl;dr

  • Flatten the Promise chain whenever possible
  • Straighten multiple nested promises with q.all() and q.spread()
  • Don’t break the Promise chain. Make sure that the last promise is returned to the caller.
  • Don’t overuse deferreds. Use them to promisify callback functions. In case a function returns a promise, it’s just better to form a chain with that same promise.

The long version

For the past one year, I have been working on 1self, a platform that unites all your personal data to provide you interesting insights. It’s been an interesting product journey, and I’ll write a separate post about that soon. We have made heavy use of Node.js to build the backend APIs for correlation and aggregation. In this post, I’d like to cover my journey of moving from callback hell to the promise(d) land.

I am assuming you know / have heard about JavaScript Promises before. If not, see the General Promise Resources on q.js wiki for beginner level resources where people smarter than me have explained the concept really well. For this article, I’ll focus on the problems I faced and the mistakes I made while using Promises so that you don’t make the same mistakes. Let’s get right to it.
—————————————————————————————————————————

1. Promise hell?

We all know what a callback hell looks like. When starting with promises, one common mistake is to write promises in callback style.

getUser("tom").then(function(user) {
  getTweets(user).then(function(tweets) {
    updateTimeline(tweets)
      .then(function() {
        console.log("done");
      });
  });
});

In the above example, we’re trying to get the user, fetch all tweets for that user and then update the timeline to display those tweets. The nesting of the getTweets() and updateTimeline() is unnecessary here. Now lets look at the example below:

getUser("tom")
  .then(getTweets)
  .then(updateTimeline)
  .then(function(){
    console.log("done");
  });

Although this looks like a contrieved example (and in a way it is) it’s possible to make such mistakes, especially if you’re used to thinking in terms of callbacks.

Conclusion

Flatten the promise chain wherever possible.
—————————————————————————————————————————

2. Nested promises

Suppose we want to associate friendship between two users. For this, we would need to fetch both the users and then connect them as friends.

fetchUser(username1)
  .then(function(user1) {
    fetchUser(username2)
      .then(function(user2) {
        makeFriends(user1, user2);
      });
});

Ideally, we want to fetch both users in parallel and when both the calls return, we want to connect them as friends.

q.all() to the rescue!

var q = require("q");

// v1
q.all([getUser(username1), getUser(username2)])
  .then(function(users) {
    makeFriends(users[0], users[1]);
  });

// v2
q.all([fetchUser(username1), fetchUser(username2)])
  .spread(function(user1, user2) {
    makeFriends(user1, user2);
  });

Here, q.all() takes an array of promises and turns it into a single promise for the whole array. In the v2 version, we have spread all those promises so that we can individually access the fulfilled values.

Conclusion

Straighten multiple nested promises with q.all() and q.spread()
—————————————————————————————————————————

3. Missing return

What does the following program print? In case you don’t know, q.delay(500) causes a delay of 500ms before executing the then block.

var q = require("q");

var getUser = function(username) {
  return q.delay(500).then(function(){
    return username + "user";
  });
};

var getTweets = function(user) {
  return q.delay(500).then(function(){
    return ["tweet1", "tweet2"];
  });
};

getUser("tom")
  .then(function(user) {
    getTweets(user)
  })
  .then(function(tweets) {
    console.log("tweets: ", tweets);
  });

Well, you might think that the output here would be tweets: ['tweet1', 'tweet2'] but if you were to run the code, you would see tweets: undefined as the output.

The reason is: we are missing a return before getTweets(user) statement.

This could be very difficult to spot, especially in a long promise chain. Hence, as a first debugging step for long promise chains, it’s best to verify that the promise chain is not broken due to missing return.

Conclusion

Don’t break the promise chain. Make sure that the last promise is returned to the caller.
—————————————————————————————————————————

4. Unnecessary use of deferred

If you have worked with callbacks a lot, chances are that you must be using q.deferred() to promisify a function accepting a callback. However, we sometimes overuse deferreds and that results in the following code:

var getUserEmail = function(username) {
  var deferred = q.defer();
  userRepository.find(username) // assume this returns a promise
    .then(function(user){
      deferred.resolve(user.email);
    });
  return deferred.promise;
};

getUserEmail("emma").then(console.log);

Here, userRepository.find(username) returns a promise, so we can fix (simplify) the code as follows:

var getUserEmail = function (username) {
  return userRepository.find(username).then(function (user) {
    return user.email;
  });
};

getUserEmail("emma").then(console.log);

Conclusion

Don’t overuse deferreds. Use them to promisify callback functions. In case a function returns a promise, it’s just better to form a chain with the same promise.
—————————————————————————————————————————

What next?

I am very fascinated by ClojureScript and core.async and plan to test ride those on a project. If you are a veteran promises user, I highly recommend checking out David Nolen’s blog post on CSP where he talks about powerful ways of handling events.

In the next post, I will talk about using promises to build asynchronous control flow patterns. I will also implement common async patterns found in async.js by using promises and JavaScript’s native collection manipulation functions (map, reduce, forEach etc). Stay tuned!

If you liked this post, you can share it with your followers or follow me on Twitter!

Chinmay Naik

Advertisements

One thought on “JavaScript Promises – Gotchas and Anti Patterns

  1. Pingback: Asynchronous Control Flow Patterns |

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: