Beware The Promise Land
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.