Beware The Promise Land

July 16, 2018

The Javascript world has embraced asynchronicity tightly. It understandably began handling its asynchronous behavior with callbacks. When those became unwieldy, the language gave us promises. We now have async await to deal with difficult promise chaining. I am currently in a project that doesn’t have easy access to async await so promises are the way forward for now.

I am working toward my Javascript Promise merit badge and today I made good progress in that endeavor. Unfortunately it was done through several hours of difficulty and pain. I had a simple misunderstanding about the different ways functions can be passed to promise chains. In hindsight it’s funny how a simple misunderstanding, under the right circumstances, can lead to big problems.

The Upshot

This:

someFunctionThatReturnsAPromise()
  .then(() => doSomething());

is NOT the same as:

someFunctionThatReturnsAPromise()
  .then(doSomething());

NOR are the above examples the same as:

someFunctionThatReturnsAPromise()
  .then(doSomething);

Let Me Explain

I believe code should be as human readable as possible. As such, I like to keep as much syntax out of it as I can. When there is an easy opportunity to clean up, I’ll take it every time. This desire can lead to trouble but I think it’s worth the effort in the long run. Here is a contrived example of what I’m talking about:

let something = {
  first: function() {
    console.log('>>>>>>>>>> first', this.value);
    return new Promise((resolve, reject) => setTimeout(resolve, 1000, 'great'));
  },

  third: function() {
    console.log('>>>>>>>>>> third:', this.value);
    return Promise.resolve();
  },

  last: function() {
    console.log('>>>>>>>>>> last:', this.value);
    return Promise.resolve();
  },

  go: function() {
    return this.first()
      .then((value) => {
        this.value = value;
        console.log('>>>>>>>>>> second:', this.value);
      })
      .then(() => this.third())
      .then(() => this.last());
  },
}

module.exports = something

The go function contains the code we’re going to focus on. The second then in the chain depends on the return value from the first promise. The third and final pieces in the chain want this.value to be set but it is not passed to them directly. Remember, this is a contrived example to illustrate the behavior of promise chaining. If you execute the go function you get the following output:

>>>>>>>>>> first undefined
>>>>>>>>>> second: great
>>>>>>>>>> third: great
>>>>>>>>>> last: great

That is… uh, great. If only I had started there. I had seen elsewhere in the codebase the omission of () => when no arguments were required. In fact, there were no parenthesis at all in some calls similar to mine. So I wrote the go function like this:

go: function() {
  return this.first()
    .then((value) => {
      this.value = value;
      console.log('>>>>>>>>>> second:', this.value);
    })
    .then(this.third)
    .then(this.last);
},

The result of this simple cleanup leads to the following output:

>>>>>>>>>> first undefined
>>>>>>>>>> second: great
>>>>>>>>>> third: undefined
>>>>>>>>>> last: undefined

The astute observer will know that I lost the context of the containing object with the change. I’m still earning my Javascript context merit badge as well so I spent a fair amount of time chasing down this context problem. The next iteration I settled on was this:

go: function() {
  return this.first()
    .then((value) => {
      this.value = value;
      console.log('>>>>>>>>>> second:', this.value);
    })
    .then(this.third())
    .then(this.last());
},

Adding the parenthesis to this.third and this.last changes it from passing a function reference to passing the executed function. A small but substantial difference to be sure. It restores the context but curiously leads to the following output:

>>>>>>>>>> first undefined
>>>>>>>>>> third: undefined
>>>>>>>>>> last: undefined
>>>>>>>>>> second: great

When I finally added the debug statements that allowed me to see this behavior I was stunned. I couldn’t believe what was happening. After making the above change (sans logs) I thought I still had a context problem. I kept trying different binding solutions to no avail while the actual problem was an asynchronous one I thought I had solved with the promise chain. The third and last functions were not waiting for the first and second functions to finish before running. They ran concurrently with the first.

The solution was to write it in the original form at the beginning of this section. I’ll repeat that here:

go: function() {
  return this.first()
    .then((value) => {
      this.value = value;
      console.log('>>>>>>>>>> second:', this.value);
    })
    .then(() => this.third())
    .then(() => this.last());
}

Lesson Learned

The key lesson for me here is that there is a small syntactic difference but large execution difference between passing a called function, a function reference, and a regular function (arrow or otherwise) to another function. It is important to recognize the difference and understand what is actually happening. Especially in an asynchronous environment.

It is clear to see now that when written this way: .then(this.third()) the third function runs then provides the result to the then function. But with a mind focused on the challenging topic of context, it was not so easy to see. Asynchronous problems are difficult enough. Mix in a little context and we have a lot of fun ahead of us.