(For a full Promise implementation, scroll down).
Some issues in your code
There are several issues, but I think the main mistake in your code is that you take the argument given to the then
method and pass it as argument to the promise constructor:
this.then=function(resolve,reject) {
var nextPromise=new Aaa(resolve,true);
// ...
Although both arguments are call back functions, they have a different signature, and serve entirely different purposes:
- The argument to the promise constructor is a call back function which is to be executed immediately, synchronously. A function is passed to it as first argument, with which you can resolve the promise you are creating.
- The (first) argument to the the
then
method, is a call back function which will only get executed later, asynchronously, when the base promise is resolved, and to which the resolved value is passed as argument.
You can see the difference also in your code, where you store the argument to the constructor as the f property. You have both this:
t.thens[i].f(g);
...where g is the resolved value, but also this:
this.f(this.resolve,this.reject);
...where the arguments are functions. When you create the nextPromise you will in fact first call f with these two arguments, and then later, with the g argument.
A Promises/A+ compliant implementation from the ground up
We could build our own Promise implementation by following the requirements in the Promises/A+ specification:
2.1 Promise states
There are only 2 state transitions allowed: from pending to fulfilled, and from pending to rejected. No other transition should be possible, and once a transition has been performed, the promise value (or rejection reason) should not change.
Note that the Promises/A+ specification says nothing about the constructor.
Here is a simple implementation that will adhere to the above restrictions. The comments reference the numbered requirements in the above specification:
function MyPromise(executor) {
this.state = 'pending';
this.value = undefined;
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch(err) { // Return a rejected promise when error occurs
this.reject(err);
}
}
// 2.1.1.1: provide only two ways to transition
MyPromise.prototype.resolve = function (value) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'fulfilled'; // 2.1.1.1: can transition
this.value = value; // 2.1.2.2: must have a value
}
MyPromise.prototype.reject = function (reason) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'rejected'; // 2.1.1.1: can transition
this.value = reason; // 2.1.3.2: must have a reason
}
Of course, this does not provide the then
method, which is key to Promises:
2.2 The then
Method
This is the core of the specification. The above code can be extended to expose the then
method, which returns a promise and provides asynchronous execution of the appropriate then
callback, only once, providing for multiple then
calls, turning exceptions to rejections, ...etc.
So the following code adds the then
method, but also a broadcast
function which is defined separately, because it must be called on any state change: this does not only include the effect of the then
method (a promise is added to a list), but also of the resolve
and reject
methods (state and value change).
function MyPromise(executor) {
this.state = 'pending';
this.value = undefined;
// A list of "clients" that need to be notified when a state
// change event occurs. These event-consumers are the promises
// that are returned by the calls to the `then` method.
this.consumers = [];
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch(err) { // Return a rejected promise when error occurs
this.reject(err);
}
}
// 2.1.1.1: provide only two ways to transition
MyPromise.prototype.resolve = function (value) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'fulfilled'; // 2.1.1.1: can transition
this.value = value; // 2.1.2.2: must have a value
this.broadcast();
}
MyPromise.prototype.reject = function (reason) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'rejected'; // 2.1.1.1: can transition
this.value = reason; // 2.1.3.2: must have a reason
this.broadcast();
}
// A promise’s then method accepts two arguments:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
var consumer = new MyPromise(function () {});
// 2.2.1.1 ignore onFulfilled if not a function
consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// 2.2.1.2 ignore onRejected if not a function
consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;
// 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise
this.consumers.push(consumer);
// It might be that the promise was already resolved...
this.broadcast();
// 2.2.7: .then() must return a promise
return consumer;
};
MyPromise.prototype.broadcast = function() {
var promise = this;
// 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved
if (this.state === 'pending') return;
// 2.2.6.1, 2.2.6.2 all respective callbacks must execute
var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';
var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';
// 2.2.4 onFulfilled/onRejected must be called asynchronously
setTimeout(function() {
// 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once
promise.consumers.splice(0).forEach(function(consumer) {
try {
var callback = consumer[callbackName];
// 2.2.1.1, 2.2.1.2 ignore callback if not a function, else
// 2.2.5 call callback as plain function without context
if (callback) {
// TODO: 2.2.7.1. For now we simply fulfill the promise:
consumer.resolve(callback(promise.value));
} else {
// 2.2.7.3 resolve in same way as current promise
consumer[resolver](promise.value);
}
} catch (e) {
// 2.2.7.2
consumer.reject(e);
};
})
});
};
This covers almost everything, except that at the TODO:
comment, the so-called Promise Resolution Procedure must be called:
2.3 The Promise Resolution Procedure
This is a procedure that treats values that are thenables (or even promises) differently: instead of returning the value as-is, the procedure will execute the then
method on that value and asynchronously fulfills the promise with the value received from that then
callback. It is not mentioned in the specs, but this is interesting to perform not only in the then
method, but also when the main promise is resolved with such a value.
So the existing resolve
method should be replaced with this "Promise Resolution Procedure", which will call the original one. The original one could be called "fulfill", to indicate it will resolve the promise always as fulfilled:
function MyPromise(executor) {
this.state = 'pending';
this.value = undefined;
// A list of "clients" that need to be notified when a state
// change event occurs. These event-consumers are the promises
// that are returned by the calls to the `then` method.
this.consumers = [];
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch(err) { // Return a rejected promise when error occurs
this.reject(err);
}
}
// 2.1.1.1: provide only two ways to transition
MyPromise.prototype.fulfill = function (value) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'fulfilled'; // 2.1.1.1: can transition
this.value = value; // 2.1.2.2: must have a value
this.broadcast();
}
MyPromise.prototype.reject = function (reason) {
if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.state = 'rejected'; // 2.1.1.1: can transition
this.value = reason; // 2.1.3.2: must have a reason
this.broadcast();
}
// A promise’s then method accepts two arguments:
MyPromise.prototype.then = function(onFulfilled, onRejected) {
var consumer = new MyPromise(function () {});
// 2.2.1.1 ignore onFulfilled if not a function
consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// 2.2.1.2 ignore onRejected if not a function
consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;
// 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise
this.consumers.push(consumer);
// It might be that the promise was already resolved...
this.broadcast();
// 2.2.7: .then() must return a promise
return consumer;
};
MyPromise.prototype.broadcast = function() {
var promise = this;
// 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved
if (this.state === 'pending') return;
// 2.2.6.1, 2.2.6.2 all respective callbacks must execute
var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';
var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';
// 2.2.4 onFulfilled/onRejected must be called asynchronously
setTimeout(function() {
// 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once
promise.consumers.splice(0).forEach(function(consumer) {
try {
var callback = consumer[callbackName];
// 2.2.1.1, 2.2.1.2 ignore callback if not a function, else
// 2.2.5 call callback as plain function without context
if (callback) {
// 2.2.7.1. execute the Promise Resolution Procedure:
consumer.resolve(callback(promise.value));
} else {
// 2.2.7.3 resolve in same way as current promise
consumer[resolver](promise.value);
}
} catch (e) {
// 2.2.7.2
consumer.reject(e);
};
})
});
};
// The Promise Resolution Procedure: will treat values that are thenables/promises
// and will eventually call either fulfill or reject/throw.
MyPromise.prototype.resolve = function(x) {
var wasCalled, then;
// 2.3.1
if (this === x) {
throw new TypeError('Circular reference: promise value is promise itself');
}
// 2.3.2
if (x instanceof MyPromise) {
// 2.3.2.1, 2.3.2.2, 2.3.2.3
x.then(this.resolve.bind(this), this.reject.bind(this));
} else if (x === Object(x)) { // 2.3.3
try {
// 2.3.3.1
then = x.then;
if (typeof then === 'function') {
// 2.3.3.3
then.call(x, function resolve(y) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.1 recurse
this.resolve(y);
}.bind(this), function reject(reasonY) {
// 2.3.3.3.3 don't allow multiple calls
if (wasCalled) return;
wasCalled = true;
// 2.3.3.3.2
this.reject(reasonY);
}.bind(this));
} else {
// 2.3.3.4
this.fulfill(x);
}
} catch(e) {
// 2.3.3.3.4.1 ignore if call was made
if (wasCalled) return;
// 2.3.3.2 or 2.3.3.3.4.2
this.reject(e);
}
} else {
// 2.3.4
this.fulfill(x);
}
}
This is now Promises/A+ compliant, at least it passes the test-suite. Yet, the Promise object exposes far too many methods and properties:
Using class
and private fields
The above built constructor creates something that is more like a Deferred object, i.e. which exposes resolve
and reject
methods. Even worse, the status
and value
properties are writable.
At the time I first posted this (2017), the concept of private fields wasn't available yet. Years later we can easily rewrite the above using class
syntax and make only then
public. Also queueMicrotask
became available:
class MyPromise {
#state = 'pending';
#value = undefined;
// A list of "clients" that need to be notified when a state
// change event occurs. These event-consumers are the promises
// that are returned by the calls to the `then` method:
#consumers = [];
// Only relevant for promises returned by a `then` call:
#onFulfilled;
#onRejected;
constructor(executor) {
try {
executor(value => this.#resolve(value),
reason => this.#reject(reason));
} catch(err) { // Return a rejected promise when error occurs
this.#reject(err);
}
}
// 2.1.1.1: provide only two ways to transition
#fulfill(value) {
if (this.#state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.#state = 'fulfilled'; // 2.1.1.1: can transition
this.#value = value; // 2.1.2.2: must have a value
this.#broadcast();
}
#reject(reason) {
if (this.#state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore
this.#state = 'rejected'; // 2.1.1.1: can transition
this.#value = reason; // 2.1.3.2: must have a reason
this.#broadcast();
}
// A promise’s then method accepts two arguments:
then(onFulfilled, onRejected) {
const consumer = new MyPromise(() => {});
// 2.2.1.1 ignore onFulfilled if not a function
consumer.#onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// 2.2.1.2 ignore onRejected if not a function
consumer.#onRejected = typeof onRejected === 'function' ? onRejected : null;
// 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise
this.#consumers.push(consumer);
// It might be that the promise was already resolved...
this.#broadcast();
// 2.2.7: .then() must return a promise
return consumer;
}
#broadcast() {
// 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved
if (this.#state === 'pending') return;
const fulfilled = this.#state == 'fulfilled';
const resolver = fulfilled ? this.#resolve : this.#reject;
// 2.2.4 onFulfilled/onRejected must be called asynchronously
queueMicrotask(() => {
// 2.2.6.1, 2.2.6.2 traverse all callbacks in order,
// 2.2.2.3, 2.2.3.3 call them only once
for (const consumer of this.#consumers.splice(0)) {
try {
const callback = fulfilled ? consumer.#onFulfilled : consumer.#onRejected;
// 2.2.1.1, 2.2.1.2 ignore callback if not a function, else
// 2.2.5 call callback without this-binding
if (callback) {
// 2.2.7.1. execute the Promise Resolution Procedure:
consumer.#resolve(callback(this.#value));
} else {
// 2.2.7.3 resolve in same way as current promise
resolver.call(consumer, this.#value);
}
} catch (e) {
// 2.2.7.2
consumer.#reject(e);
};
}
});
};
// The Promise Resolution Procedure: will treat values that are thenables/promises
// and will eventually call either fulfill or reject/throw.
#resolve(x) {
// 2.3.1
if (this === x) {
throw new TypeError('Circular reference: promise value is promise itself');
}
// 2.3.2
if (x instanceof MyPromise) {
// 2.3.2.1, 2.3.2.2, 2.3.2.3
x.then(value => this.#resolve(value),
reason => this.#reject(reason));
} else if (x === Object(x)) { // 2.3.3
// firstTime() only returns true the first time it's called
let firstTime = () => (firstTime = () => false, true);
try {
// 2.3.3.1
const then = x.then;
if (typeof then === 'function') {
// 2.3.3.3
then.call(x, (y) => {
// 2.3.3.3.3 don't allow multiple calls
// 2.3.3.3.1 recurse
if (firstTime()) this.#resolve(y);
}, (reasonY) => {
// 2.3.3.3.3 don't allow multiple calls
// 2.3.3.3.2
if (firstTime()) this.#reject(reasonY);
});
} else {
// 2.3.3.4
this.#fulfill(x);
}
} catch(e) {
// 2.3.3.3.4.1 ignore if call was made
// 2.3.3.2 or 2.3.3.3.4.2
if (firstTime()) this.#reject(e);
}
} else {
// 2.3.4
this.#fulfill(x);
}
}
}
This also successfully passes the Promises/A+ test suite.
There are several optimisations possible to this code, such as merging similar code into shorter code blocks, but as it stands now it shows quite clearly where each requirement is covered.
Happy coding.