Update / caveat
2015-07-24
I just had it wrong about this. See my next article: "JavaScript: expectations re-adjusted re Promises". Ignoring this side of things, the rest of this article does correctly reflect the mechanics of Promises, I think.
2021-01-14
I've just come back to this, and feel the previous update - originally buried halfway through the article - was not as emphatic as it should have been. My understanding / expectations of how Promises in Javascript work are just wrong in this article. I considered taking it down completely, but decided to leave it here. There's a lesson to be learned here: at the point at which things were not behaving how I'd expected, I should have done more research, rather than plouging ahead with even more wrongness. Oh well.
Thanks to Ryan for setting me straight in his comment.
My recommendation is to only read this if you want to laugh at me being extensively wrong. There is very little value in it other than that.
I've messed around with a Future-ish sort of construct in the past ("CFML: Threads, callbacks, closure and a pub"), but never looked at Promises before, beyond reading the JavaScript docs for them ("Promise"), and being intrigued.
Way back when all the Acker-fracas was taking place I started knocking together some code to get me up to speed with JavaScript's implementation, just so I knew how they worked, and also to verify my suggestion had merit for CFML. I parked the code due to being sidetracked by other things, but I figured I'd write it up now. First I need to remind myself what all this code does.
According to the JavaScript docs, a Promise is:
TheI'm always rubbish at understand docs, so my reaction to that is "that's nice". I figured I needed to write some code and observe the behaviour, then I'd get a handle on what was going on.Promise
object is used for deferred and asynchronous computations. APromise
represents an operation that hasn't completed yet, but is expected to in the future.
[...]
APromise
represents a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers to an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of the final value, the asynchronous method returns a promise of having a value at some point in the future.
The syntax of promise usage is:
new Promise(executor);
new Promise(function(resolve, reject) { ... });
Basically the constructor takes an argument that is a call back function, which itself takes two arguments (both call backs themselves), which can be called by the executor in either success or failure situations.
Promises
A minimal (and pointless) example of a Promise being used is thus:// promise.js
promise = new Promise(
function(resolve, reject) {
// stuff goes here
}
);
console.dir(promise);
If we inspect the
promise
variable we don't get much that's useful:
Promise
__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
The promise is "pending" because I've not resolved it (by calling the
resolve()
call back within its code: I'll get to that).One thing that took me an age to "get" is that the value of the promise object is not really important. It's not the value of the Promise one is ever after, it's just a mechanism to call tasks asynchronously. And I'm not doing that here.
If I modify my code to have some telemetry, we can see something interesting:
console.log("Before promise");
new Promise(
function(resolve, reject) {
console.log("Promise with no resolve()");
}
);
console.log("After promise");
Running this gives:
Before promise
Promise with no resolve()
After promise
Hmmm. This is my first assumption dashed. I expected the "executor" of the Promise would be called asynchronously, but this doesn't seem to be the case here.
The code is executed in the sequence it's encountered. I thought perhaps it's because the executor code runs so quickly that it gets in "under the wire" before the next statement in the calling code executes. So I decided to slow the executor down a bit to see what happens.
// slowWithNoResolve.js
slowThing = function(){
console.log("Beginning of slowThing() process");
var startTime=new Date();
var words = ulyssesExtract.replace(/(\b\w+\b)(?=.*\1)/gi, "").replace(/\s+/g," ");
var endTime = new Date();
var elapsed = endTime - startTime;
console.log("After slowThing() process (" + elapsed + "ms)\n\n\n");
};
var startTime=new Date();
console.log("Before promise");
new Promise(
function(resolve, reject) {
console.log("Start of executor call back");
slowThing();
console.log("End of executor call back");
}
);
var endTime = new Date();
var elapsed = endTime - startTime;
console.log("After promise (" + elapsed + "ms)\n\n\n");
This outputs:
Before promise
Start of executor call back
Beginning of slowThing() process
After slowThing() process (2845ms)
End of executor call back
After promise (2850ms)
BTW, the
ulyssesExtract
string is in ulysses.js, which is simply the last chapter of Ulysses (full text here: "Episode 18 - Penelope"). It's a big wodge of text (115kB or thereabouts) which would take a while to do a string replacement on. My use of this chapter of Ulysses, btw, is intended to be a portent of the length and accessibility of this article ;-)So here again we see that the executor is executed immediately, but in a blocking fashion: it's not called asynchronously, as we can see it complete before the next line of the calling code is run. And the execution time of the calling code includes the execution time of the Promise's task. What I was expecting to see was along these lines:
Before promise
After promise (5ms)
Start of executor call back
Beginning of slowThing() process
After slowThing() process (2845ms)
End of executor call back
That'd indicate the executor was being executed asynchronously. This left me puzzled. But we'll get to that too.
resolve()
As it stands, the Promise object is unresolved. This is because we didn't call resolve()
in the executor. If we call it, we see the promise actually resolves:// defaultResolve.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with default resolve()");
resolve("OK");
}
);
console.log("After promise");
console.dir(p);
If we look at the output of the console.dir()
call now, we see this:Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: "OK"
So it's being resolved. Cool. Note I did not actually pass a
resolve()
call back in... Promises have their own resolve method which is used in lieu of a passed in one.
then()
How does one pass a resolve call back into the promise? This is done via calling then()
on the promise object, and passing in a call back, eg:// handledResolve.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled resolve()");
resolve("OK");
}
).then(function(value){
console.log("resolve() provided via then() using value " + value);
});
console.log("After promise");
console.dir(p);
This outputs:
Before promise
Promise with handled resolve()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: undefined
resolve() provided via then() using value OK
And, hey! It's running asynchronously! So what gives there? It seems to me - and I don't think this is clear from the docs - that the executor of the Promise doesn't seem to do anything async, but what you do by way of resolving the Promise is run async. So the handlers in the
then()
calls are async. If I'm wrong about this, someone please let me know... this is what is borne out by my test code though.
reject()
The executor call back takes two arguments: the first is a call back which will be used to resolve()
the promise, the second is if it needs to be rejected. A rejection situation is what it sounds like: one might have code like this:new Promise(function(resolve, reject){
var result = getSomeThing();
if (result != null){
resolve(result);
}else{
reject("didn't get anything back");
}
});
So - as per above - if we get something back for a result, then we continue the next step in whatever process we need to undertake via the resolve handler, and that keeps trucking away in the background. However if we didn't get any result, we have nothing to do, so we
reject()
the promise.Here's the previous example rejigged to reject the promise instead of resolving it:
// handledReject.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled reject()");
reject("Not OK");
}
).then(null, function(reason){
console.log("reject() provided via then() using reason " + reason);
return Promise.reject(reason);
});
console.log("After promise");
console.dir(p);
And now we get this output:
Before promise
Promise with handled reject()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "rejected"
[[PromiseValue]]: "Not OK"
reject() provided via then() using reason Not OK
Uncaught (in promise) Not OK
Notice in this situation I'm making a point of returning a rejected Promise from the rejection handler. Because I'm not then dealing with the rejection somehow, I get an error bubbling back.
I deal with this tidily by taking some remedial action (whatever that might be) in the rejection handler, then returning a resolved Promise:
// handledRejectWithResolve.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled reject()");
reject("Not OK");
}
).then(null, function(reason){
console.log("reject() provided via first then() using reason " + reason);
return Promise.reject(reason);
}).then(null, function(reason){
console.log("reject() provided via second then() using reason " + reason);
// perform some remedial action
return Promise.resolve("Rejected promise (" + reason + ") now resolved");
});
console.log("After promise");
console.dir(p);
And output:
Before promise
Promise with handled reject()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: "Rejected promise (Not OK) now resolved"
reject() provided via first then() using reason Not OK
reject() provided via second then() using reason Not OK
Note that the error raised by the rejection isn't a catchable one:
// handledRejectWithTryCatch.js
console.log("Before promise");
try {
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled reject()");
try {
reject("Not OK");
} catch (e){
console.log("Caught execption in mainline");
console.dir(e);
}
}
).then(null, function(reason){
console.log("reject() provided via first then() using reason " + reason);
return Promise.reject(reason);
});
} catch(e){
console.log("Caught execption in mainline");
console.dir(e);
}
console.log("After promise");
console.dir(p);
This yields:
Before promise
Promise with handled reject()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "rejected"
[[PromiseValue]]: "Not OK"
reject() provided via first then() using reason Not OK
Uncaught (in promise) Not OK
Neither a
try
/catch
around the reject()
call, nor one around the whole thing, catches this exception.
catch()
Notice how in my then()
call, if I am only dealing with a rejection, I have to pass null as the first argument as the first argument is the resolve handler:
).then(null, function(reason){
console.log("reject() provided via first then() using reason " + reason);
return Promise.reject(reason);
});
There's a shortcut for this situation: using
catch()
instead of then:// rejectWithCatch.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled reject()");
reject("Not OK");
}
).catch(function(reason){
console.log("reject() provided via catch() using reason " + reason);
return Promise.reject(reason);
});
console.log("After promise");
console.dir(p);
Result:
Before promise
Promise with handled reject()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "rejected"
[[PromiseValue]]: "Not OK"
reject() provided via first catch() using reason Not OK
Uncaught (in promise) Not OK
There's not much else to say about that.
Chaining
You might have noticed in the example slightly further up that I chained a secondthen()
call to the first one. This works because a then()
call itself returns a Promise.// twoThens.js
console.log("Before promise");
p = new Promise(
function(resolve, reject) {
console.log("Promise with handled resolve()");
resolve("OK");
}
).then(function(value){
console.log("resolve() provided via first then() using value " + value);
}).then(function(value){
console.log("resolve() provided via second then() using value " + value);
});
console.log("After promise");
console.dir(p);
The
new Promise()
call obviously returns a promise; and I call its then()
method... which also returns a second Promise, and I then call its then()
. And the outcome is predictable by now:Before promise
Promise with handled resolve()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: undefined
resolve() provided via first then() using value OK
resolve() provided via second then() using value undefined
One thing to note is that if one does not return a specific Promise from one of the
resolve()
or reject()
handlers, then a resolved Promise is returned. Here I chain two then()
calls, and even though I reject()
the Promise in the executor, and the rejection handler is called, because that handler doesn't return an explicit promise it defaults to returning a resolved Promise, so in the second then()
call, the resolve()
handler is called, not the reject()
one:// twoThensWithRejection.js
executor = function(resolve, reject) {
console.log("Promise with handled resolve()");
reject("Not OK");
};
resolveHandler = function(value){
console.log("resolve() using value " + value);
};
rejectHandler = function(value){
console.log("reject() using reason " + value);
};
console.log("Before promise");
p = new Promise(executor)
.then(resolveHandler, rejectHandler)
.then(resolveHandler, rejectHandler);
console.log("After promise");
console.dir(p);
Output:
Before promise
Promise with handled resolve()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: undefined
reject() using reason Not OK
resolve() using value undefined
Also note that by default the
resolve()
handler does not receive a message if one relies on the default-return Promise.This approach could be fine... the rejection handler might tidy up whatever the problem was and that's cool: the promise is resolved. If however one wishes to continue the promise chain's rejection handling, then one needs to return an explicitly rejected Promise:
// twoThensWithContinuedRejection.js
executor = function(resolve, reject) {
console.log("Promise with handled resolve()");
reject("Not OK");
};
resolveHandler = function(value){
console.log("resolve() using value " + value);
};
firstRejectHandler = function(value){
console.log("reject() using reason " + value);
return Promise.reject("Explicit rejection");
};
secondRejectHandler = function(value){
console.log("reject() using reason " + value);
return Promise.resolve("Explicit resolution");
};
console.log("Before promise");
p = new Promise(executor)
.then(resolveHandler, firstRejectHandler)
.then(resolveHandler, secondRejectHandler);
console.log("After promise");
console.dir(p);
Result:
Before promise
Promise with handled resolve()
After promise
Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: "Explicit resolution"
reject() using reason Not OK
reject() using reason Explicit rejection
This also uses two of the Promise class's static methods:
Promise.resolve()
and Promise.reject()
. These don't resolve or reject a Promise, they return a resolved or rejected Promise.
all()
and race()
I have to admit I'm a bit bamboozled by a coupla things I'm seeing here, so I would take with a pinch of salt what I say.The Promise API has another two methods:
all()
and race()
.
all()
The docs for Promise.all()
say this:
The Promise.all(iterable)
method returns a promise that resolves when all of the promises in the iterable argument have resolved.
Fair enough. So I pass all()
an array of Promises, and once they're all resolved, the Promise that all()
returns is resolved. This would be useful if one had a bunch of unrelated processes to kick off, at the same time, but one has to wait until they're all done before continuing. This is similar to in CFML kicking off lots of thread calls, and then having a thread-join afterwards.Let's have a look at an example:
// all.js
promiseGenerator = {};
promiseGenerator[Symbol.iterator] = function*(){
var count = 0;
while (++count <= 5){
console.log("yielding with (" + count + ")");
yield getIndexedPromise(count);
}
};
getIndexedPromise = function(index){
return new Promise(function(resolve,reject){
console.log("(" + index + ") Promise executor");
resolve(index);
}).then(function(value){
slowThing("(" + value + ") then() resolver");
return Promise.resolve(value);
});
};
console.log("Before finalPromise is created");
finalPromise = Promise.all(promiseGenerator);
console.log("Before finalPromise has then() attached");
finalPromise.then(function(value){
console.dir(value);
console.log("finalPromise's then()'s resolve() called with " + value);
});
console.log("End of processing");
Here I've got an iterator that uses a generator to sequentially create a series of five Promises. Each does my Ulysses trick to emulate slow processing in their
resolve()
handler. I pass the iterator to Promise.all()
and it will run each Promise in turn. When all the Promises in the iterator are resolved, the Promise returned by the Promise.all()
call will - itself - resolve, and in doing so the resolution handler passed to then()
will be called. When I run this, I get this output:Before finalPromise is created
yielding with (1)
(1) Promise executor
yielding with (2)
(2) Promise executor
yielding with (3)
(3) Promise executor
yielding with (4)
(4) Promise executor
yielding with (5)
(5) Promise executor
Before finalPromise has then() attached
End of processing
(1) then() resolver
Beginning of slowThing("(1) then() resolver") process
After slowThing("(1) then() resolver") process (2872ms)
(2) then() resolver
Beginning of slowThing("(2) then() resolver") process
After slowThing("(2) then() resolver") process (2867ms)
(3) then() resolver
Beginning of slowThing("(3) then() resolver") process
After slowThing("(3) then() resolver") process (2868ms)
(4) then() resolver
Beginning of slowThing("(4) then() resolver") process
After slowThing("(4) then() resolver") process (2867ms)
(5) then() resolver
Beginning of slowThing("(5) then() resolver") process
After slowThing("(5) then() resolver") process (2877ms)
Array[5]
0: 1
1: 2
2: 3
3: 4
4: 5
length: 5
__proto__: Array[0]
finalPromise's then()'s resolve() called with 1,2,3,4,5
This is as one would expect, output-wise, as the
finalPromise
gets the results of all the promises (the array of 1-5). However what is bemusing me is that the whole thing should take about 3sec to run: the slowThing()
process takes that long, but I should be running the promises in parallel. However processing actually takes about 17sec to run. So the Promises aren't running in parallel.Initially I thought this was perhaps a limitation of Chrome's JS engine: it not emulating multi-threading very well, so I knocked together a version to run on Node.js instead, and its behaviour was much the same as Chrome's, other than being substantially faster.
I'll need to dig into this.
race()
race()
is similar to all()
. The only difference is that the Promise the race()
call returns resolves after the first resolved Promise in the passed-in array, rather than waiting for them all to complete.My sample code for
race()
is identical to that for all()
, except it calls race()
instead. I'll not bother repeating it here (but it's on GitHub: race.js).The results here are not what I expected (I'm running this one via Node):
Before promises are created
(1) Promise executor
(2) Promise executor
(3) Promise executor
(4) Promise executor
(5) Promise executor
After promises are created
Before finalPromise is created
Before finalPromise has then() attached
End of processing
(1) then() resolver
Beginning of slowThing("(1) then() resolver") process
After slowThing("(1) then() resolver") process (1927ms)
(2) then() resolver
Beginning of slowThing("(2) then() resolver") process
After slowThing("(2) then() resolver") process (1933ms)
(3) then() resolver
Beginning of slowThing("(3) then() resolver") process
After slowThing("(3) then() resolver") process (1935ms)
(4) then() resolver
Beginning of slowThing("(4) then() resolver") process
After slowThing("(4) then() resolver") process (1937ms)
(5) then() resolver
Beginning of slowThing("(5) then() resolver") process
After slowThing("(5) then() resolver") process (1943ms)
1
finalPromise's then()'s resolve() called with 1
/src/js/promises/stubJs>
What bemuses me here is that the
finalPromise
doesn't seem to actually resolve()
- ie: its code to run - until after all the Promises in the array are resolved. That's not how race()
is supposed to work. What's I'd expect is this:Before promises are created
(1) Promise executor
(2) Promise executor
(3) Promise executor
(4) Promise executor
(5) Promise executor
After promises are created
Before finalPromise is created
Before finalPromise has then() attached
End of processing
(1) then() resolver
Beginning of slowThing("(1) then() resolver") process
After slowThing("(1) then() resolver") process (1927ms)
1
finalPromise's then()'s resolve() called with 1
(2) then() resolver
Beginning of slowThing("(2) then() resolver") process
After slowThing("(2) then() resolver") process (1933ms)
[etc]
IE: as soon as the first Promise in the array is resolved, the
finalPromise
is resolved, and this should take place before the second (and subsequent) promises need to resolve. I guess I am misunderstanding stuff here, and I have more research to do. Or my readership can set me straight, if they know where I'm going wrong.CFML
I started this exercise to demonstrate to myself that my suggestion to add Promises to CFML would have merit ("Futures/Promises" (4010501)). It's clear to me that they do, as they basically represent a pleasingly OO approach to handling what we might do procedurally with thread calls at the moment. And, obviously, it's more "async-aware" in its approach to handling the async nature of the handlers. This would be a cool feature of CFML.It was a bloody interesting exercise, actually!
Righto.
--
Adam