Basic Javascript promise implementation attempt
Asked Answered
B

5

70

To gain better understanding of how promises work in Javascript I decided to give it a try and code basic implementation myself.

Basically I want to implement Promises Object (I call it Aaa in my code) that takes function as an argument. This function can call resolve to resolve the promise, or reject to reject it. The basic implementation and usage is below. Not sure if the second argument is accepteable according to promise specs, but that's what I got so far.

Aaa=function(f,pause) { 

    console.log("ggg");

    var t=this;
    this.f=f;
    this.thens=[];

    this.resolve=function(g) {

        for(var i=0;i<t.thens.length;i++)
        {
            // try/catch to be used later for dealing with exceptions

            try
            {
                t.thens[i].f(g);
                t.thens[i].resolve();
            }   
            catch(ex)
            {}

        }
    };  

    // to be implemented later
    this.reject=function(g) {};

    this.then=function(resolve,reject) {

        // i'm passing true for pause argument as we dont need to execute promise code just yet
        var nextPromise=new Aaa(resolve,true);

        this.thens.push(nextPromise);

        return nextPromise;
    }


    if(!pause)
        this.f(this.resolve,this.reject); 

}


var aaa=new Aaa(function(resolve,reject) {

    console.log("aaa");

    setTimeout(function() {

        console.log("fff");
        resolve("good");

    },2000);

    console.log("bbb");

});

So now the promise can be created, called and resolved. Each then method will return new Aaa (Promise) so these can be chained. Now the code below uses promise created above and chains then callbacks. Each then returns new promise and in this case it seems to work fine:

aaa.then(function(res) {

    console.log("ccc");
    console.log(res);

})
.then(function(res) {
    console.log("ddd");
    console.log(res);
},function(rej) {
    console.log("eee");
    console.log(rej);
});

the output I'm getting is:

ggg
aaa 
bbb 
ggg 
ggg 
fff 
ccc 
good 
ddd 
undefined 

The problem is however when one of the then calls returns a promise:

aaa.then(function(res) {

    console.log("ccc");
    console.log(res);

    // here we return the promise manually. then next then call where "ddd" is output should not be called UNTIL this promise is resolved. How to do that?

        return new Aaa(function(resolve,reject) {

        console.log("iii");

        setTimeout(function() {
        console.log("kkk");
            resolve("good2");
            // reject("bad");

        },2000);

        console.log("jjj");

    }).then(function (res) {
        console.log("lll");

        console.log(res);
    });

})
.then(function(res) {
    console.log("ddd");
    console.log(res);
},function(rej) {
    console.log("eee");
    console.log(rej);
});

The output is:

ggg 
aaa 
bbb 
ggg 
ggg  
fff  
ccc  
good  
ggg  
iii  
jjj  
ggg  
ddd  
undefined  
kkk  
lll  
good2 

The call then where ddd is output should not be called UNTIL the returned promise we just added is resolved.

How would that be best implemented?

Badajoz answered 21/5, 2014 at 1:57 Comment(1)
Related.Barina
R
97

There are a number of cases you're not handling here. The best bet is to start by building the promise as a state machine:

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];
}

Now lets define a simple helper to use through the rest of our implementation:

// a function that returns `then` if `value` is a promise, otherwise `null`
function getThen(value) {
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    var then = value.then;
    if (typeof then === 'function') {
      return then;
    }
  }
  return null;
}

Next, we need to consider each of the transformations that can occur:

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }
}

Note how resolve can receive a Promise as its argument, but a Promise can never be fulfilled with another Promise. So we have to handle this special case.

Note also that a Promise can only ever be fulfilled/rejected once. We also have the problem that a third party Promise may misbehave, and we should guard our code against that. For this reason, I haven't just called result.then(resolve, reject) from within resolve. Instead, I split that into a separate function:

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 */
function doResolve(fn, onFulfilled, onRejected) {
  var done = false;
  try {
    fn(function (value) {
      if (done) return
      done = true
      onFulfilled(value)
    }, function (reason) {
      if (done) return
      done = true
      onRejected(reason)
    })
  } catch (ex) {
    if (done) return
    done = true
    onRejected(ex)
  }
}

So now we have a completed state machine, but no way to observe or trigger the changes in state. Lets start by adding a way to trigger the state changes by passing in a resolver function.

function Promise(fn) {
  if (typeof this !== 'object')
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function')
    throw new TypeError('fn must be a function');

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }

  doResolve(fn, resolve, reject);
}

As you can see, we re-use doResolve because we have another un-trusted resolver. The fn might call resolve or reject multiple times, and it might throw an error. We need to handle all of these cases (and that's what doResolve does).

We now have the completed state machine, but we haven't exposed any information about what state it is in. Lets try adding a .done(onFulfilled, onRejected) method that is just like .then except that it does not return a Promise and does not handle errors thrown by onFulfilled and onRejected.

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise(fn) {
  if (typeof this !== 'object')
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function')
    throw new TypeError('fn must be a function');

  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers
  var handlers = [];

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      state = FULFILLED;
      value = result;
      handlers.forEach(handle);
      handlers = null;
    } catch (e) {
      reject(e);
    }
  }

  function reject(error) {
    state = REJECTED;
    value = error;
    handlers.forEach(handle);
    handlers = null;
  }

  function handle(handler) {
    if (state === PENDING) {
      handlers.push(handler);
    } else {
      if (state === FULFILLED && typeof handler.onFulfilled === 'function') {
        handler.onFulfilled(value);
      }
      if (state === REJECTED && typeof handler.onRejected === 'function') {
        handler.onRejected(value);
      }
    }
  }
  this.done = function (onFulfilled, onRejected) {
    setTimeout(function () { // ensure we are always asynchronous
      handle({
        onFulfilled: onFulfilled,
        onRejected: onRejected
      });
    }, 0);
  }

  doResolve(fn, resolve, reject);
}

Note how we must handle the case of .done being called both before and after the Promise becomes fulfilled/rejected.

We almost have a complete promise implementation, but, as you already noticed when building your own implementation, we need a .then method that returns a Promise.

We can build this easilly out of .done:

this.then = function (onFulfilled, onRejected) {
  var self = this;
  return new Promise(function (resolve, reject) {
    return self.done(function (result) {
      if (typeof onFulfilled === 'function') {
        try {
          return resolve(onFulfilled(result));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return resolve(result);
      }
    }, function (error) {
      if (typeof onRejected === 'function') {
        try {
          return resolve(onRejected(error));
        } catch (ex) {
          return reject(ex);
        }
      } else {
        return reject(error);
      }
    });
  });
}

Note here how we get the thing you were struggling with for free now, because resolve accepts a Promise and waits for it to be resolved.

N.B. I haven't tested this Promise implementation (although it is correct to the best of my knowledge). You should test any implementation you build against the Promises/A+ test suite (https://github.com/promises-aplus/promises-tests) and may also find the Promises/A+ spec (https://github.com/promises-aplus/promises-spec) useful in figuring out what the correct behavior is for any specific part of the algorithm. As a final resource, promise is a very minimal implementation of the Promise spec.

Raptorial answered 21/5, 2014 at 13:51 Comment(15)
I've cross posted this answer as an article on promisejs.org/implementingRaptorial
For the states, I'd rather use ANTICIPATING, FULFILLED, and BROKEN. :PCelebes
You can call them whatever you like internally :) These are the words used in the specification.Raptorial
Isn't Promise.then a setter of the eventual action and not the action itself? How could you use value.then and return it as the action itself? I didn't get that part. Can you clarify?Fluecure
Something like then is a class level method of Promise, and the real stuff resides in some variable inside the promise instance (and not Promise Class i.e value in this case).Fluecure
then is a method on promise instances. It's used to extract the value of the promise (by registering handlers) and to transform the promise by returning a new promise. It's very similar to Array.prototype.map, except for promises.Raptorial
When is resolve actually called? The function passed to the promise is invoked by doResolve, but it is passed a function (that seems to call resolve) that is never explicitly called. Please explain this.Yurikoyursa
It's up to the underlying asynchronous operation to call resolve or reject at the appropriate time (i.e. once the operation has completed). doResolve is the user supplied function in which the actual work is done, hence it is passed the resolve and reject fns. Promises are typically used as an abstraction over callbacks or event emitters etc. so it would depend on the underlying operation as to exactly what this is. promisejs.org guides you through how you would use this as an abstraction over fs.readFile from node.jsRaptorial
If the value passed to resolve is a promise that is properly implemented, its .then method should not let exceptions surface. It also does not seem possible that getThen would throw an exception. It seems that the only statement in the try block in resolve that could throw an exception is handlers.forEach(handle);, and only when the handler throws an exception. Would not it be inappropriate to catch such an exception because if the handler were passed to .done, its exception could not surface. The api states that exceptions thrown by handlers passed to .done should surface.Joel
Of course my previous comment only applies when the function, fn, passed to Promise calls resolve after some asynchronous action so that the handlers passed to .done are pushed onto the handlers array before the fn can call resolve from within doResolve.Joel
The value passed to resolve could be a misbehaving promise, e.g. {get then() { throw new Error("this is thrown when getting the .then property"); }}Raptorial
You may be right about the handlers though, this isn't a well tested implementation.Raptorial
Is this worth implementing nowadays ? I mean, should we continue this way or use async / await with our promises ? This method seems very old schoolGloomy
@VolontéduPeuple, you are commenting on a question from 2014. Of course, since ECMAScript2015 we have a native Promise object, and all modern JavaScript implementations have it. However, as the OP starts out, their main drive to write this was "to gain better understanding". That is a noble cause for doing this, even today. By the number of questions related to promises, we can see that there is a lot of misunderstanding, irrespective of whether they use async and await or then.Ultramicroscope
Alright, my coworkers keep to argue this solution was still good to implement instead of native async / await, and since they will not ear what I say ...Gloomy
U
37

(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.

Ultramicroscope answered 5/2, 2017 at 22:8 Comment(5)
The best explanation of promise implementation that I've seen so far.Brabant
could you please explain purpose of broadcast function and consumer?Weide
@bikashamit, there is some explanation of that already in my answer and code comments. In simple terms: the consumers are the promises that are returned by the then and catch calls on the promise object. These need to know when to call the callback functions that were provided to then and catch as arguments. The appropriate callback needs to be called when the promise resolves. The broadcast function takes care of notifying these consumers, so they can take care of making that call, and act upon it.Ultramicroscope
What does this line do? handlers.forEach(handle) It seems to just loop through each handle without doing anythingRika
@lolcatapril24, that line does not appear in my code. I think your comment was about another answer. But anyway: forEach will call the given function (handle) for each entry of the array (handlers), passing it the array element as argument.Ultramicroscope
D
1

my solution

function Promise(resolver){
    if(typeof resolver !== 'function') {
        throw new TypeError(`Promise resolver ${resolver} is not a function`)
    }
    this.state = 'pending'
    this.value = void 0
    try{
        resolver(this.resolve.bind(this), this.reject.bind(this))
    }catch(error){
        this.reject.call(this,error)
    }
}

Promise.prototype.resolve = function(value) {
    if(this.state !== 'pending') return
    this.value = value
    this.state = 'fulfilled'    
    setTimeout( () => {
        if(!this.onFulfilled) return
        this.onFulfilled(value)
    }, 0)
};

Promise.prototype.reject = function(reason){
    if(this.state !== 'pending') return
    this.value = reason
    this.state = 'rejected'
    setTimeout( () => {
        if(this.onRejected){
            this.onRejected(reason)
        }else{
            throw `Uncaught (in promise) ${reason}`
        }
    }, 0)
};

Promise.prototype.then = function(fulfilled, rejected){
    if ( typeof fulfilled !== 'function' && typeof rejected !== 'function' ) {
        return this;
    }
    if (typeof fulfilled !== 'function' && this.state === 'fulfilled' ||
        typeof rejected !== 'function' && this.state === 'rejected') {
        return this;
    }
    var self = this
    return new Promise( (resolve, reject) => {
        if(fulfilled && typeof fulfilled == "function"){
            var onFulfilled = function (){
                try{
                    var result = fulfilled(self.value)
                    if(result && typeof result.then === 'function'){
                        result.then(resolve, reject)
                    }else{
                        resolve(result)
                    }
                }catch(error){
                    reject(error)
                }
            }
            if(self.state === 'pending'){
                self.onFulfilled = onFulfilled
            }else if(self.state === 'fulfilled'){
                onFulfilled()
            }
        }
        if(rejected && typeof rejected == "function"){
            var onRejected = function (){
                try{
                    var result = rejected(self.value)
                    if(result && typeof result.then === 'function'){
                        result.then(resolve, reject)
                    }else{
                        resolve(result)
                    }
                }catch(error){
                    reject(error)
                }
            }
            if( self.state === 'pending'){
                self.onRejected = onRejected
            }else if(self.state === 'rejected'){
                onRejected()
            }
        }
    })
}

/*
 *  the methods don't in Promise/A+ 
 */
Promise.prototype.catch = function(onRejected){
    return this.then(null, onRejected)
}

Promise.all = function(iterable){
    if(typeof iterable[Symbol.iterator] !== 'function'){
        throw new TypeError(`${iterable[Symbol.iterator]} is not a function`)
    }
    // Array,TypedArray,String,arguments ==> length; Map,Set ==> size 
    let len = [...iterable].length, i = 0, counter = 0, res = [];
    return new Promise( (resolve, reject) => {
        for(let item of iterable){
            ( (i) => {
                Promise.resolve(item).then(function(value){
                    counter++
                    res[i] = value
                    if(counter == len){
                        resolve(res)
                    }
                },function(reason){
                    if(!called){
                        reject(reason)
                    }
                })
            })(i++)
        }
    })
}

Promise.race = function(iterable){
    if(typeof iterable[Symbol.iterator] !== 'function'){
        throw new TypeError(`${iterable[Symbol.iterator]} is not a function`)
    }
    return new Promise( (resolve,reject) => {
        for(let item of iterable){
            Promise.resolve(item).then(function(value){
                resolve(value)
            },function(reason){
                reject(reason)
            })
        }
    })
}

Promise.resolve = function(value){
    //if(value instanceof this) return value
    //if(value instanceof Promise) return value
    if(value.constructor !== Promise) return value
    return new Promise( (resolve,reject) => {
        if(value && typeof value === 'object' && typeof value.then === 'function'){
            resolve( value.then( v => v))
        }else{
            resolve(value)
        }
    })
}

Promise.reject = function(reason){
    return new Promise( (resolve,reject) => {
        reject(reason)
    })
}
Dillondillow answered 6/3, 2018 at 7:18 Comment(0)
F
0

I tried to implement this with ES6. Posting because it might be useful for someone else

class MyPromise {
  _value = null;
  _isRejected = false;
  _fullFilled = false;
  _handlers = [];
  _errorHandlers = [];
  _error = null;

  constructor(func) {
    func(this._resolve, this._reject);
  }

  _resolve = (value) => {
    this._value = value;
    this._fullFilled = true;
    this._handlers.forEach(handler => handler(value));
  };

  _reject = (error) => {
    this._isRejected = true;
    this._error = error;
    this._errorHandlers.forEach(errorHandler => errorHandler(error));
  };

  catch(errorHandler){
    return new MyPromise((resolve, reject) => {
      this._errorHandler(resolve, reject, errorHandler)
    })
  }

  _errorHandler(resolve, reject, callback){
    const runErrorHandler = () => {
      let error;
      let returnedFromCatchCallback;
      try{
        returnedFromCatchCallback = callback(this._error);
      }catch(_error){
        error = _error;
        reject(error);
      }
      resolve(returnedFromCatchCallback);
    };

    if(this._isRejected){
      runErrorHandler(this._error);
    }

    this._errorHandlers.push(runErrorHandler);
  }

  then(handler, errorHandler) {
    const returnOfHandler = new MyPromise((resolve, reject) => {
      const runHandler = (value) => {
        try{
          resolve(handler(value));
        }catch(error){
          reject(error);
        }
      };
      this._handlers.push(runHandler);

      if(this._fullFilled) {
        runHandler(this._value);
      }

      this._errorHandler(resolve, reject, errorHandler);
    });

    return returnOfHandler;
  }
}

export default MyPromise;
Fluecure answered 20/5, 2020 at 18:13 Comment(2)
Some issues: this can be resolved multiple times (even be both fulfilled and rejected - and worse, the _errorHandler even does that), resolve is not recursively dealing with thenables, then does not deal with omitted callbacks, and it leaks memory by holding onto callbacks that are no longer needed.Salesclerk
@Salesclerk Yes, I realize there are multiple issues with this.I plan to try this again.Thanks for feedback.Fluecure
R
-2

this all seems extremely complicated. I think there's a really simple recursive solution. I'm going to omit the reject for the sake of brevity, but it's pretty much the same as resolve except you halt the chain.

var MyPromise = function(callback) {
  this.callbacks = [];
  callback(this.resolve.bind(this));
 }

MyPromise.prototype.resolve = function(data) {
  var callback = this.callbacks.pop();
  var result =  callback(data);

  if (!result) return;

  if (result instanceof MyPromise) {
    var resolve = this.resolve.bind(this);
    return result.then(function(d) {
        return resolve(d);
    });
  }

  return this.resolve(result);

}

MyPromise.prototype.then = function(callback) {
  this.callbacks.unshift(callback);
  return this;
}
Resurrection answered 22/7, 2017 at 20:54 Comment(1)
this will fail with a simple non-async code resolving the value and does not support chaining and no rejection. basically its not a promise.Engleman

© 2022 - 2024 — McMap. All rights reserved.