Understanding the Promises/A+ specification
What is a promise?
A promise is a thenable
whose behavior conforms to the Promises/A+ specification.
A thenable
is any object or function that has a then
method.
Here's what a promise looks like:
var promise = {
...
then: function (onFulfilled, onRejected) { ... },
...
};
This is the only thing we know about a promise from the outset (excluding its behavior).
Understanding the Promises/A+ specification
The Promises/A+ specification is divided into 3 main parts:
- Promise states
- The
then
Method - The Promise Resolution Procedure
The specification does not mention how to create, fulfill or reject promises.
Hence, we'll start by creating those functions:
function deferred() { ... } // returns an object { promise, resolve, reject }
function fulfill(promise, value) { ... } // fulfills promise with value
function reject(promise, reason) { ... } // rejects promise with reason
Although there's no standard way of creating a promise yet the tests require us to expose a deferred
function anyway. Hence, we'll only use deferred
to create new promises:
deferred()
: creates an object consisting of{ promise, resolve, reject }
:promise
is a promise that is currently in the pending state.resolve(value)
resolves the promise withvalue
.reject(reason)
moves the promise from the pending state to the rejected state, with rejection reasonreason
.
Here's a partial implementation of the deferred
function:
function deferred() {
var call = true;
var promise = {
then: undefined,
...
};
return {
promise: promise,
resolve: function (value) {
if (call) {
call = false;
resolve(promise, value);
}
},
reject: function (reason) {
if (call) {
call = false;
reject(promise, reason);
}
}
};
}
N.B.
- The
promise
object only has athen
property which is currentlyundefined
. We still need to decide on what thethen
function should be and what other properties a promise object should have (i.e. the shape of a promise object). This decision will also affect the implementation of thefulfill
andreject
functions. - The
resolve(promise, value)
andreject(promise, value)
functions should only be callable once and if we call one then we shouldn't be able to call the other. Hence, we wrap them in a closure and ensure that they are only called once between both of them. - We introduced a new function in the definition of
deferred
, the promise resolution procedureresolve(promise, value)
. The specification denotes this function as[[Resolve]](promise, x)
. The implementation of this function is entirely dictated by the specification. Hence, we'll implement it next.
function resolve(promise, x) {
// 2.3.1. If promise and x refer to the same object,
// reject promise with a TypeError as the reason.
if (x === promise) return reject(promise, new TypeError("Self resolve"));
// 2.3.4. If x is not an object or function, fulfill promise with x.
var type = typeof x;
if (type !== "object" && type !== "function" || x === null)
return fulfill(promise, x);
// 2.3.3.1. Let then be x.then.
// 2.3.3.2. If retrieving the property x.then results in a thrown exception e,
// reject promise with e as the reason.
try {
var then = x.then;
} catch (e) {
return reject(promise, e);
}
// 2.3.3.4. If then is not a function, fulfill promise with x.
if (typeof then !== "function") return fulfill(promise, x);
// 2.3.3.3. If then is a function, call it with x as this, first argument
// resolvePromise, and second argument rejectPromise, where:
// 2.3.3.3.1. If/when resolvePromise is called with a value y,
// run [[Resolve]](promise, y).
// 2.3.3.3.2. If/when rejectPromise is called with a reason r,
// reject promise with r.
// 2.3.3.3.3. If both resolvePromise and rejectPromise are called,
// or multiple calls to the same argument are made,
// the first call takes precedence, and any further calls are ignored.
// 2.3.3.3.4. If calling then throws an exception e,
// 2.3.3.3.4.1. If resolvePromise or rejectPromise have been called, ignore it.
// 2.3.3.3.4.2. Otherwise, reject promise with e as the reason.
promise = deferred(promise);
try {
then.call(x, promise.resolve, promise.reject);
} catch (e) {
promise.reject(e);
}
}
N.B.
- We omitted section 2.3.2 because it's an optimization that depends upon the shape of a promise object. We'll revisit this section towards the end.
- As seen above, the description of section 2.3.3.3 is much longer than the actual code. This is because of the clever hack
promise = deferred(promise)
which allows us to reuse the logic of thedeferred
function. This ensures thatpromise.resolve
andpromise.reject
are only callable once between both of them. We only need to make a small change to thedeferred
function to make this hack work.
function deferred(promise) {
var call = true;
promise = promise || {
then: undefined,
...
};
return /* the same object as before */
}
Promise states and the then
method
We've delayed the problem of deciding the shape of a promise object for so long but we can't delay any further because the implementations of both the fulfill
and reject
functions depend upon it. It's time to read what the specification has to say about promise states:
A promise must be in one of three states: pending, fulfilled, or rejected.
- When pending, a promise:
- may transition to either the fulfilled or rejected state.
- When fulfilled, a promise:
- must not transition to any other state.
- must have a value, which must not change.
- When rejected, a promise:
- must not transition to any other state.
- must have a reason, which must not change.
Here, “must not change” means immutable identity (i.e.
===
), but does not imply deep immutability.
How do we know which state the promise is currently in? We could do something like this:
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
var promise = {
then: function (onFulfilled, onRejected) { ... },
state: PENDING | FULFILLED | REJECTED, // vertical bar is not bitwise or
...
};
However, there's a better alternative. Since the state of a promise is only observable through it's then
method (i.e. depending upon the state of the promise the then
method behaves differently), we can create three specialized then
functions corresponding to the three states:
var promise = {
then: pending | fulfilled | rejected,
...
};
function pending(onFulfilled, onRejected) { ... }
function fulfilled(onFulfilled, onRejected) { ... }
function rejected(onFulfilled, onRejected) { ... }
In addition, we need one more property to hold the data of the promise. When the promise is pending the data is a queue of onFulfilled
and onRejected
callbacks. When the promise is fulfilled the data is the value of the promise. When the promise is rejected the data is the reason of the promise.
When we create a new promise the initial state is pending and the initial data is an empty queue. Hence, we can complete the implementation of the deferred
function as follows:
function deferred(promise) {
var call = true;
promise = promise || {
then: pending,
data: []
};
return /* the same object as before */
}
In addition, now that we know the shape of a promise object we can finally implement the fulfill
and reject
functions:
function fulfill(promise, value) {
setTimeout(send, 0, promise.data, "onFulfilled", value);
promise.then = fulfilled;
promise.data = value;
}
function reject(promise, reason) {
setTimeout(send, 0, promise.data, "onRejected", reason);
promise.then = rejected;
promise.data = reason;
}
function send(queue, callback, data) {
for (var item of queue) item[callback](data);
}
We need to use setTimeout
because according to section 2.2.4 of the specification onFulfilled
or onRejected
must not be called until the execution context stack contains only platform code.
Next, we need to implement the pending
, fulfilled
and rejected
functions. We'll start with the pending
function which pushes the onFulfilled
and onRejected
callbacks to the queue and returns a new promise:
function pending(onFulfilled, onRejected) {
var future = deferred();
this.data.push({
onFulfilled: typeof onFulfilled === "function" ?
compose(future, onFulfilled) : future.resolve,
onRejected: typeof onRejected === "function" ?
compose(future, onRejected) : future.reject
});
return future.promise;
}
function compose(future, fun) {
return function (data) {
try {
future.resolve(fun(data));
} catch (reason) {
future.reject(reason);
}
};
}
We need to test whether onFulfilled
and onRejected
are functions because according to section 2.2.1 of the specification they are optional arguments. If onFulfilled
and onRejected
are provided then they are composed with the deferred value as per section 2.2.7.1 and section 2.2.7.2 of the specification. Otherwise, they are short-circuited as per section 2.2.7.3 and section 2.2.7.4 of the specification.
Finally, we implement the fulfilled
and rejected
functions as follows:
function fulfilled(onFulfilled, onRejected) {
return bind(this, onFulfilled);
}
function rejected(onFulfilled, onRejected) {
return bind(this, onRejected);
}
function bind(promise, fun) {
if (typeof fun !== "function") return promise;
var future = deferred();
setTimeout(compose(future, fun), 0, promise.data);
return future.promise;
}
Interestingly, promises are monads as can be seen in the aptly named bind
function above. With this, our implementation of the Promises/A+ specification is now complete.
Optimizing resolve
Section 2.3.2 of the specification describes an optimization for the resolve(promise, x)
function when x
is determined to be a promise. Here's the optimized resolve
function:
function resolve(promise, x) {
if (x === promise) return reject(promise, new TypeError("Self resolve"));
var type = typeof x;
if (type !== "object" && type !== "function" || x === null)
return fulfill(promise, x);
try {
var then = x.then;
} catch (e) {
return reject(promise, e);
}
if (typeof then !== "function") return fulfill(promise, x);
// 2.3.2.1. If x is pending, promise must remain pending until x is
// fulfilled or rejected.
if (then === pending) return void x.data.push({
onFulfilled: function (value) {
fulfill(promise, value);
},
onRejected: function (reason) {
reject(promise, reason);
}
});
// 2.3.2.2. If/when x is fulfilled, fulfill promise with the same value.
if (then === fulfilled) return fulfill(promise, x.data);
// 2.3.2.3. If/when x is rejected, reject promise with the same reason.
if (then === rejected) return reject(promise, x.data);
promise = deferred(promise);
try {
then.call(x, promise.resolve, promise.reject);
} catch (e) {
promise.reject(e);
}
}
Putting it all together
The code is available as a gist. You can simply download it and run the test suite:
$ npm install promises-aplus-tests -g
$ promises-aplus-tests promise.js
Needless to say, all the tests pass.
Possible contradiction between Promises/A+ spec and ECMAScript promises?
Let's suppose
x
happened to be a promise itself, then the steps must be taken is the following: […]
No, they don't need to be taken - they only may be taken if x
is a "promise". These steps are an optional ("allowed", not "required") optimisation:
Note 4:
Generally, it will only be known thatx
is a true promise if it comes from the current implementation. This clause allows the use of implementation-specific means to adopt the state of known-conformant promises.
ECMAScript does not treat its own Promise
s as "known to be conformant", ignoring these steps. They simply treat native promises like all other thenables. Given there is no way to create an ECMAScript Promise
that is fulfilled with another promise, this is equivalent to directly adopting the state.
Where does say in ECMAScript that resolved promise has [[PromiseResult]] equal to object?
Step 9 gets the then
property
- Let then be Get(resolution, "then").
There are mainly four outcomes possible:
then
is not a property of the object, so then (the variable) represents a completion record with [[Value]]undefined
then
is a property of the object, but evaluating it produces an exception (e.g. it is a getter, and the getter runs into an exception). then is then a so-called abrupt completion.then
is a property of the object, but it is not a functionthen
is a property of the object, and it is a function.
Step 10 checks for one of these outcomes:
- If then is an abrupt completion, then
a. Return RejectPromise(promise, then.[[Value]]).
Here is a snippet that makes 10.a
happen:
let obj = {
get then() {
throw "sorry";
}
};
new Promise(resolve => resolve(obj))
.catch((err) => console.log("rejected with ", err))
JS Promises: does the then() method always return a promise, or can it return a thenable?
It's not a promise unless its .then(…)
method returns a promise, and if it's an ES6 native Promise
then it definitely will.
A thenable's then
method may return anything (including undefined
).
promise-like objects and then-able duck typing in javascript promise
A. instanceof
Technically, the Javascript expression a instanceof b
checks
a
is an object (which includes functions because they are objects),- 'b' is a constructor function - which includes classes because declarations and expressions using the
class
keyword create constructor functions, - the value of
b.prototype
is in the inheritance chain ofa
. This means eithera
was constructed byb
, or otherwise somewhere in the inheritance chain ofa
there is an object constructed byb
.
Given an objects inheritance chain is set to the prototype
property of its constructor, aPromise
in your code is an instance of Promise
because Promise.prototype
is the first entry in its prototype chain. The second entry in its prototype chain is Object.prototype
so aPromise
is an instance of Object
as well.
a
is constructed and initialized by Object
in response to the {..}
object initializer syntax. Object.prototype
is the first object in its inheritance so its an "instance of" Object
and nothing else.
B. Thenable objects
"Thenable" objects, with a .then
method, are described in the A+ promise specification on which ES6 promises are based.
Recognition and treatment of thenables allow different software libraries, each using its own Promise constructor code, to deal with promises created by another library created using a different constructor. Simply checking if a promise object is an instance of a library's own "Promise" constructor prevents inter-library usage of promise objects.
The one time a promise implementation needs to deal with a "thenable" object, is when a foreign library promise is used to resolve a promise created by an implementation. The standard method of dealing with this is for the implementation to create a new pair of resolve/reject functions, say resolve2
and reject2
for the promise being resolved, and call the thenable's then
method to pass on it's settled value. In pseudo code:
if promiseA, instance of PromiseA, is resolved with promiseB, instance of PromiseB {
PromiseA code creates new resolve2/reject2 functions for promiseA
and calls the thenable's then method as:
promiseB.then( resolve2, reject2);
}
// ....
If promiseB remains pending, promiseA remains pending;
If promiseB is settled, promiseA becomes fulfilled or rejected with the same value.
How ES2015 Promise actually works when resolving another thenable?
This is a standard behaviour in promise chaining, which means ES2015 promise library recognises that you're resolving a promise with another promise and waits for that promise to resolve, then continues with the chain, which is why in the callback, you get the actual value instead of the promise object you resolved with earlier.
In the second case however, you're wrapping the promise in an object which prevents promise library to recognise it as a promise, so it just passes it to the next callback.
Understanding Promise constructor
In fact, the first argument of the promise constructor callback is called resolve
, not fulfill
.
When you pass a promise or thenable into resolve
, its state will get adopted, all other values fulfill the promise directly. This means that exactly the logic you were describing is implemented right within resolve
- and, as you said, you don't need the condition in the resolver callback.
And in fact, the standard specifies Promise.resolve
to be implemented quite like
function resolve(x) {
return new Promise(function(resolve, reject) {
resolve(x);
});
}
Related Topics
Angularjs Changes Urls to "Unsafe:" in Extension Page
Cloud Firestore Case Insensitive Sorting Using Query
Angularjs - How to Use $Routeparams in Generating the Templateurl
How to Execute Promises Sequentially, Passing the Parameters from an Array
Angularjs 1.2 $Injector:Modulerr
What Is Ajax and How Does It Work
How to Set Locale in Datepipe in Angular 2
Adding Custom Functions into Array.Prototype
Best Cross-Browser Method to Capture Ctrl+S with Jquery
How to Create Dynamic Variable Names Inside a Loop
Getting Scroll Bar Width Using JavaScript
Activexobject in Firefox or Chrome (Not Ie!)
JavaScript - Cannot Set Property of Undefined
Typeerror: Console.Log(...) Is Not a Function
How to Call Loading Function with React Useeffect Only Once
Tolocaledatestring() Changes in Ie11