web stats

Promises/A+ – understanding the spec through implementation

NB: This is for promises/A+ v1. The spec has since moved to v1.1. The below is still a good introduction. There are now slides available with an implementation of v1.1.

What we’re going to do is create a promises/A+ implementation based on http://promises-aplus.github.io/promises-spec/. By doing this hopefully we’ll get a deeper understanding of just how promises work. I’ll call this Aplus and put it up on github under https://github.com/rhysbrettbowen/Aplus

First some boilerplate. Let’s make Aplus an object:

Aplus = {};

Promise States

from http://promises-aplus.github.io/promises-spec/#promise_states there are three states: pending, fulfilled and rejected. It does not state the value of these states, so let’s enumerate them:
var State = {
PENDING: 0,
FULFILLED: 1,
REJECTED: 2
};

var Aplus = {
state: State.PENDING
};
you will see that I’ve also put in the default state for our promise as pending.
now we need to be able to transition from a state. There are some rules around what transitions are allowed – mostly that when we transition from pending to any other state we can’t transition again. Also when transitioning to a fulfilled state we need a value, and a reason for rejected.

according to the terminology http://promises-aplus.github.io/promises-spec/#terminology a value can be anything including undefined and a reason is any value that indicates why a promise was rejected. That last definition is a little blurry – can “undefined” indicate why something was rejected? I’m going to say no and only accept non-null values. If anything doesn’t work then I’ll throw an error. So let’s create a “chageState” method that handles the checking for us:

var State = {
PENDING: 0,
FULFILLED: 1,
REJECTED: 2
};

var Aplus = {
state: State.PENDING,
changeState: function(state, value) {

// catch changing to same state (perhaps trying to change the value)
if ( this.state == state ) {
throw new Error("can't transition to same state: " + state);
}

// trying to change out of fulfilled or rejected
if ( this.state == State.FULFILLED ||
this.state == State.REJECTED ) {
throw new Error("can't transition from current state: " + state);
}

// if second argument isn't given at all (passing undefined allowed)
if ( state == State.FULFILLED &&
arguments.length < 2 ) {
throw new Error("transition to fulfilled must have a non null value");
}

// if a null reason is passed in
if ( state == State.REJECTED &&
value == null ) {
throw new Error("transition to rejected must have a non null reason");
}

//change state
this.state = state;
this.value = value;
return this.state;
}
};

Now we’re on to the fun stuff.

Then

This is where the usefulness of the promise comes in. The method handles all it’s chaining and is the way we add new functions on to the list. First up let’s get a basic then function that will check if the fulfilled and rejected are functions and then store them in an array. This is important as 3.2.4 says that it must return before invoking the functions so we need to store them somewhere to execute later. Also we need to return a promise so let’s create the promise and store that with the functions in an array:

then: function( onFulfilled, onRejected ) {

// initialize array
this.cache = this.cache || [];

var promise = Object.create(Aplus);

this.cache.push({
fulfill: onFulfilled,
reject: onRejected,
promise: promise
});

return promise;
}

Resolving

Next let’s concentrate on what happens when we actually resolve the promise. Let’s again try and take the simple case and we’ll add on the other logic as we go. First off we either run the onFulfilled or onRejected based on the promise state and we must do this in order. We then change the status of their associated promise based on the return values. We also need to pass in the value (or reason) that we got when the state changed. Here is a first pass:
resolve: function() {
// check if pending
if ( this.state == State.PENDING ) {
return false;
}

// for each 'then'
while ( this.cache && this.cache.length ) {
var obj = this.cache.shift();

// get the function based on state
var fn = this.state == State.FULFILLED ? obj.fulfill : obj.reject;
if ( typeof fn != 'function' ) {
fn = function() {};
}

// fulfill promise with value or reject with error
try {
obj.promise.changeState( State.FULFILLED, fn(this.value) );
} catch (error) {
obj.promise.changeState( State.REJECTED, error );
}
}
}

This is a good first pass. It handles the base case for normal functions. The two other cases we need to handle though are when we’re missing a function (at the moment we’re using a blank function but we really need to pass along the value or the reason with the correct state) and when they return a promise. Let’s first tackle the problem of passing along an error or value when we’re missing a function:

resolve: function() {
// check if pending
if ( this.state == State.PENDING ) {
return false;
}

// for each 'then'
while ( this.cache && this.cache.length ) {
var obj = this.cache.shift();

var fn = this.state == State.FULFILLED ? obj.fulfill : obj.reject;


if ( typeof fn != 'function' ) {

obj.promise.changeState( this.state, this.value );

} else {

// fulfill promise with value or reject with error
try {
obj.promise.changeState( State.FULFILLED, fn(this.value) );
} catch (error) {
obj.promise.changeState( State.REJECTED, error );
}

}

}
}

If the function doesn’t exist we’re essentially passing along the state and the value. One thing that hit me when reading through this is that if you are using a onRejected function and you want to pass along the error state to the next promise is you’ll have to throw another error, otherwise the promise will resolve with the returned value. I guess that this is a good thing as you can essentially use onRejected to “fix” errors by doing things like returning a default value.

There is only one thing left in resolving and that’s to handle what happens when a promise is returned. The spec gives an example of how to do this at: http://promises-aplus.github.io/promises-spec/#point-65 so let’s put it in

resolve: function() {
// check if pending
if ( this.state == State.PENDING ) {
return false;
}

// for each 'then'
while ( this.cache && this.cache.length ) {
var obj = this.cache.shift();

var fn = this.state == State.FULFILLED ? obj.fulfill : obj.reject;


if ( typeof fn != 'function' ) {

obj.promise.changeState( this.state, this.value );

} else {

// fulfill promise with value or reject with error
try {

var value = fn( this.value );

// deal with promise returned
if ( value && typeof value.then == 'function' ) {

value.then( function( value ) {
obj.promise.changeState( State.FULFILLED, value );
}, function( reason ) {
obj.promise.changeState( State.REJECTED, error );
});
// deal with other value returned
} else {
obj.promise.changeState( State.FULFILLED, value );
}
// deal with error thrown
} catch (error) {
obj.promise.changeState( State.REJECTED, error );
}
}
}
}

Asynchronous

So far so good, but there are two bits we haven’t dealt with. The first is that the onFulfilled and onRejected functions should not be called in the same turn of the event loop. To fix this we should only add our “then” functions to the array after the event loop. We can do this through things like setTimeout or process.nextTick. To make this easier we’ll put on a method that will run a given function asynchronously so it can be overridden with whatever implementation you use. For now though we’ll use setTimeout though you can use nextTick or requestAnimationFrame
async: function(fn) {
setTimeout(fn, 5);
}

The last step is putting in when to resolve. There should be two cases when we need to check, the first is when we add in the ‘then’ functions as the state might already be set. This gives us a then method looking like:

then: function( onFulfilled, onRejected ) {

// initialize array
this.cache = this.cache || [];

var promise = Object.create(Aplus);
var that = this;
this.async( function() {
that.cache.push({
fulfill: onFulfilled,
reject: onRejected,
promise: promise
});
that.resolve();
});

return promise;
}

and the second should be when the state is changed so add a this.resolve() to the end of the changeState function. Wrap it all in a function that will use Object.create to get you a promise and the final code will look like this:

Final

var Aplus = function() {

var State = {
PENDING: 0,
FULFILLED: 1,
REJECTED: 2
};

var Aplus = {
state: State.PENDING,
changeState: function( state, value ) {

// catch changing to same state (perhaps trying to change the value)
if ( this.state == state ) {
throw new Error("can't transition to same state: " + state);
}

// trying to change out of fulfilled or rejected
if ( this.state == State.FULFILLED ||
this.state == State.REJECTED ) {
throw new Error("can't transition from current state: " + state);
}

// if second argument isn't given at all (passing undefined allowed)
if ( state == State.FULFILLED &&
arguments.length < 2 ) {
throw new Error("transition to fulfilled must have a non null value");
}

// if a null reason is passed in
if ( state == State.REJECTED &&
value == null ) {
throw new Error("transition to rejected must have a non null reason");
}

//change state
this.state = state;
this.value = value;
this.resolve();
return this.state;
},
fulfill: function( value ) {
this.changeState( State.FULFILLED, value );
},
reject: function( reason ) {
this.changeState( State.REJECTED, reason );
},
then: function( onFulfilled, onRejected ) {

// initialize array
this.cache = this.cache || [];

var promise = Object.create(Aplus);

var that = this;

this.async( function() {
that.cache.push({
fulfill: onFulfilled,
reject: onRejected,
promise: promise
});
that.resolve();
});

return promise;
},
resolve: function() {
// check if pending
if ( this.state == State.PENDING ) {
return false;
}

// for each 'then'
while ( this.cache && this.cache.length ) {
var obj = this.cache.shift();

var fn = this.state == State.FULFILLED ?
obj.fulfill :
obj.reject;


if ( typeof fn != 'function' ) {

obj.promise.changeState( this.state, this.value );

} else {

// fulfill promise with value or reject with error
try {

var value = fn( this.value );

// deal with promise returned
if ( value && typeof value.then == 'function' ) {
value.then( function( value ) {
obj.promise.changeState( State.FULFILLED, value );
}, function( error ) {
obj.promise.changeState( State.REJECTED, error );
});
// deal with other value returned
} else {
obj.promise.changeState( State.FULFILLED, value );
}
// deal with error thrown
} catch (error) {
obj.promise.changeState( State.REJECTED, error );
}
}
}
},
async: function(fn) {
setTimeout(fn, 5);
}
};

return Object.create(Aplus);

};

you might have noticed I also put in functions “fulfill” and “reject”. The spec doesn’t say anything about how to manually change the state of a promise. Other names may be used like “fail”, “resolve” or “done” but I’m using “fulfill” and “reject” to keep in line with the specs and what they call their two functions.

Next time

In future I’ll write a bit more about some patterns you can use promises for, like passing around data, making requests in parallel and caching. Promises are really powerful but they also come at a cost so I’ll outline all the pros and cons and what their alternatives are in different situations, but for now hopefully this sheds some light on the internals of how a promise works.

*edit* Looks like the tests https://github.com/promises-aplus/promises-tests don’t like errors thrown so I’ve changed the changeState to instead return the errors, not throw and the tests allow null reasons for errors so I’ve changed that and uploaded to github

Powered by WPeMatico