Simplify Promise chains with Arrow Functions

Arrow Functions are a new feature of JavaScript slated for arrival in ES6. They provide new syntax for creating function expressions. In the JavaScript world, we frequetly create function expressions when we make anonymous functions or define a function. Arrow Functions give us another tool.

At the time of this article, Arrow Functions have limited support on browsers. They are not supported by Node.js, even with the --harmony build flag. If you want to try the examples fire up the latest version of Firefox.

So what does an Arrow Function look like? Consider the code below that uses the Array.map function to square the values in the Array.

var nums = [1,2,3];

// create array of squares using a function definition
var squareFunc = function squareFunc(x) { return x * x; });  
var squares0 = nums.map(squareFunc);

// create array of squares using an anonymous function
var squares1 = nums.map(function(x) { return x * x; });

// create array of squares using an arrow function
var squares2 = nums.map(x => x * x);  

As you can see, the Arrow Function provides similar functionality to function definitions and anonymous functions, just with simpler syntax. For a more detailed work-up on Arrow Functions syntax, I recommend reading the MDN Documentation or the Harmony Proposal.

So what's the point? Arrow Functions come with two major benefits:

  1. Lexical scoping! This means that scope of the caller retained inside the function. With Arrow functions you don't have to create this closures or use bind!
  2. Simpler syntax because return is implicit when there are no curly braces

While the first benefit is HUGE for simplifying code, this article is going to focus on the second. In particular, the improved syntax gains when chaining Promises.

Lets consider the following broken code:

var myLib = {  
  logAsync: function(val) {  
    setTimeout(function() { 
      console.log('Logging: ' + val) 
    }, Math.random() * 1000);      
  }      
};

mylib.logAsync(1);  
mylib.logAsync(2);  
mylib.logAsync(3);  
mylib.logAsync(4);  
mylib.logAsync(5);  

We have an object myLib that contains a single function logAsync. This function will output at a random inteval between 0 and 1 seconds. The results, as expected, are arbitrary; we'll get different output values every time we run the code.

If we want to force these to execute in order we can rewrite this code to use Promises. In this example, I'll use ES6's Promise object.

var myLib = {  
  logAsync: function(val) {
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
        console.log('Logging: ' + val)
        resolve(val);
      }, Math.random() * 1000);
    });
  }
}

Now that our function is using Promises, we can then execute calls sequentially using the Promise object's then function:

myLib.logAsync(1)  
.then(function() { return myLib.logAsync(2); })
.then(function() { return myLib.logAsync(3); })
.then(function() { return myLib.logAsync(4); })
.then(function() { return myLib.logAsync(5); });

Mission accomplished! We've solved the issues of creating sequential execution. But to me this is pretty ugly. There is a lot of code to simply chain the calls.

So lets replace the existing chaining code with with Arrow Functions and see how it looks...

myLib.logAsync(1)  
.then(() => myLib.logAsync(2))
.then(() => myLib.logAsync(3))
.then(() => myLib.logAsync(4))
.then(() => myLib.logAsync(5));

Sweeeet!

So why is the first version ugly? For starters, the creation of the anonymous function is verbose.

But the most aggregious part is the need to use return. The return is required because logAsync is returning a Promise. We want to delay execution until the Promise is resolved.

If you don't return the logAsync Promise from your handler, the handler will be fulfilled immediately and we lose sequential execution.

This part of working with Promises is complicated and requires you to have a solid understanding of how then actually works. It's not for beginners.

Why I like the Arrow Function syntax is because the return is implicit. So chained Promise calls are a bit less nuanced. I believe this will help make the use of Promises less error prone and easier to work with.

So we've managed to use Arrow Functions. But what if we want to access the fulfillment value of the Promise? Lets start by extending the myLib library to include a logPrevAsync function:

var myLib = {  
  /* logAsync defined previous */
  logPrevAsync: function(val, prev) {
    console.log('Previous: ' + prev);
    return this.logAsync(val);
  }
}

This new method will simply output the previous value and then call the logAsync function.

With Arrow Functions, we can supply arguments that get passed to the function expression.

The then function of a Promise will pass the resolution value as the argument to its handler. We can create an Arrow Function with a single argument that will represent the previous Promise's resolution value:

myLib.logPrevAsync(1)  
.then((prev) => myLib.logPrevAsync(2, prev))
.then((prev) => myLib.logPrevAsync(3, prev))
.then((prev) => myLib.logPrevAsync(4, prev))
.then((prev) => myLib.logPrevAsync(5, prev));

The result is that our code now outputs:

"Previous: undefined"
"Logging: 1"
"Previous: 1"
"Logging: 2"
"Previous: 2"
"Logging: 3"
"Previous: 3"
"Logging: 4"
"Previous: 4"
"Logging: 5"

So why does this any of this matter? I believe this helps improve the readibility of Promises. Promises are still a complicated concept to grok. When constructing complex Promise chains it's easy to leave out a return and spend hours tracking down the issue.

With Arrow Functions we have one less thing to worry about. Combined with the shortened syntax (no function or curly braces), it makes working with Promises simpler.

These changes will hopefully make Promises more accessible to beginner and intermediate JavaScript developers.

Other Attempts at Improving Readability

I'd be remiss to not include a few other "techniques" to combat Promise readibility complexity.

One technique is to use bind to create function expressions.

myLib.logAsync(1)  
.then(myLib.logAsync.bind(this, 2))
.then(myLib.logAsync.bind(this, 3))
.then(myLib.logAsync.bind(this, 4))
.then(myLib.logAsync.bind(this, 5));

To me, this feels clunky. It definitely works, but some of the semantic nature of the code is lost. Basically, it isn't immediately obvious that you're creating function expressions unless you're familiar with how bind works.

Another technique is creating a utility class to "wrap" your method calls into function expressions.

var myLibExpressions = {  
  logAsync: function(val) {
    return function() {
      return myLib.logAsync(val);
    }
  },
  logPrevAsync: function(val) {
    return function(prev) {
      return myLib.logPrevAsync(val, prev);
    }
  }
};

The calling syntax is definitely cleaner...

myLib.logAsync(1)  
.then(myLibExpr.logAsync(2))
.then(myLibExpr.logAsync(3))
.then(myLibExpr.logAsync(4))
.then(myLibExpr.logAsync(5));

But you end up with two sets of the same methods: one used for calling myLib directly and one that referencing myLib function expressions. In my opinion, this is unnecessary complexity for the sake of readibility.

How do you write Promise chains? What do you think about Arrow Functions?

comments powered by Disqus