Understanding Promise by creating your own : Part 3

Pankaj Bhageria
JavaScript Bytes
Published in
6 min readOct 5, 2018

In earlier parts to this we built our own MyPromise Class/generator which had quite a few capabilities of Promise.

Part 1 : https://medium.com/javascript-bytes/understanding-promise-by-creating-your-own-part-1-d98d47720bbc

Part 2: https://medium.com/javascript-bytes/understanding-promise-by-creating-you-own-part-2-bf0a1a0a42a4

In this part we will focus on very important feature of Promise, chaining of Promises. Without chaining Promise would have been more or less similar to callbacks when dealing with multiple async operations.

One of the major drawbacks of using callbacks is callback hell. Basically when you have to do multiple async operations one after another then each call is a nested call in the previous callback. Lets understand this through an example.

Say we have a add function which asynchronously adds 2 numbers.

function add(a,b,callback){
setTimeout(function(){
callback(a+b);
},100);
}

and lets say we have add 1,2,3,4

add(1,2,function(result1){
add(result1,3,function(result2){
add(result2, 4,function(result3){
console.log(result3);
})
})
})

So above you can see how the callbacks are nested. This makes the code very unreadable and difficult to maintain.

Lets see the same example with Promise. So say we have a addP function adds 2 number asynchronously and returns a promise.

function addP(a,b){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve(a+b);
},100)
});
}

Using the above function addP, lets solve the problem.

addP(2,4).then(function(result){

addP(result,5).then(function(result2){

addP(result2,6).then(result3){
console.log(result3);
}
})
})

So using Promise in this way doesn’t seem to solve the problem.

Lets use the chaining feature of Promise to solve the above problem

addP(1,2).then(function(result1){
return addP(result1,3)
})
.then(function(result2){
return addP(result2,4);
})
.then(result3){
console.log(result3);
})

So the callback hell problem finally seems to be solved with chaining Promise.

//So here we see that we are returning a new Promise in the then callback.

Lets see how does promise chaining work in above example.

addP(1,2) returns a promise say p1.

  1. then method on p1 accepts the callback and returns a new promise say p2.
  2. the then callback of p1 returns another promise p3(addP(result1,3)). (note that p2 is the return value of then method, and p3 is the return value of the callback passed to then method).
  3. when p3 resolves (which is result1 +3), then p2 also resolves with the same value.
  4. this chain continues

In the above example the return value of the then callback is always a promise. In case if its just another value then p2 will resolve with that value. Lets see that with an example.

addP(1,2).then(function(result1){
console.log(result1); //3
return result + 1;
})
.then(function(result2){
console.log(result2) // 4;
})

So effectively we can say following things about the then method

  1. the then method(on promise p) accepts a callback(c)
  2. the then method returns a new Promise(pthen).
  3. when p resolves callback c is called whose return value is x.
  4. pthen resolves with the value x.
  5. if the x is again a promise then pthen resolves when x resolves( so x resolves to give value y and pthen resolves with y)

Lets try to implement this in MyPromise

In the implementation of MyPromise up till now, the then method returns the same promise object on which it was called . But to implement chaining we will need it to return an new promise.

Before going ahead with implementing this, we would need to learn one trick.

Generally we resolve the Promise from inside the executor function we pass to the promise. To implement this case we need to resolve the promise outside of the executor function.

let resolver;
let p = new Promise(function(resolve,reject){
resolver = resolve
});
//somewhere else
resolver(2);

In fact there is an abstraction called deferred which exactly for this purpose. Its available out of the box in jQuery library. We can extend our MyPromise to create that.

function Deferred(){ let p = new MyPromise((resolve,reject)=>{
this.resolve = resolve;
this.reject = reject;
}); this.p = p;}let d = new Deferred();d.promise; //refers to the promise
d.resolve; //refers to the resolver function
d.reject //refers to the reject function.

So lets start extend our previous implementation. For now we will modify the then part to make it chainable. We will look at the catch part later.

function Deferred(){ let p = new MyPromise((resolve,reject)=>{
this.resolve = resolve;
this.reject = reject;
}); this.promise = p;}
function MyPromise(executor){
this.state = 'initial';
this.value = undefined;
this.onValue = [];
this.onError = [];
executor(resolve.bind(this),reject.bind(this));
}
MyPromise.prototype = {
then: function(handler){
let deferred = new Deferred();if(this.state === 'initial'){
this.onValue.push({deferred:deferred, handler:handler});
}
else if(this.state === 'resolved'){
sendResponse({deferred:deferred, handler:handler},this.value);
}
else{
//state is rejected, don't do anything
}
return deferred.promise;
},
catch:function(handler){
if(this.state === 'initial'){
this.onError.push(handler);
}
else if(this.state === 'rejected'){
sendResponse(handler,this.error);
}
else{
//promise is resolved do nothing
}
return this;
}
}//please not that below are not global function but private inside //our file
function resolve(value){
this.state = 'resolved';
this.value = value;
this.onValue.map(nextObj=>sendResponse(nextObj,value));
}
function reject(error){
this.state = 'rejected';
this.error = error;
this.onError.map(handler => sendResponse(handler,error));
}
function sendResponse(nextObj,response){setImmediate(function(){
let handlerValue = nextObj.handler(response);
if(handlerValue instanceof MyPromise){
handlerValue.then(function(value){
nextObj.deferred.resolve(value);
})
}
else{
nextObj.deferred.resolve(response);
}
})

So here whenever then is called we create a new Promise using Deferred and return that promise. At the same time we push the deferred along with our handler so that when the Promise resolves, we can call the handler and then resolve the returned Promise via the deferred object.

So the then chaining should work fine using this method. Lets try to fix the implementation of catch. But before that lets first see how will catch work in case of chaining.

Previously we were attaching then and catch to the same promise object. But now that is not possible as the then promise will return a new promise. So basically every promise will have either a then or a catch handler attached. Lets see how that would work.

Lets first see how error handling works in case of chaining

addP(1,2) //p1
.then(function(result1){
return addP(result1,3)
}) //p2
.then(function(result2){
return addP(result2,4);
}) //p3
.then(function(result3){
console.log(result3);
}) //p4
.catch(function(error){
console.log(error);
})

So here we see that we have attached only one catch statement. When any promise in the chain fails, then the catch handler is executed. Lets understand how does this work.

  1. addP(1,2) returns a promise p1.
  2. similarly p1.then() returns p2, p2.then() returns p3, p3.then() returns p4.
  3. say p1 is rejected
  4. as no catch handler is attached to it, it doesn’t execute the then handler, and just rejects p2
  5. similarly p2 rejects p3, and p3 rejects p4.
  6. finally at p4 we find a catch handler and that is executed.

More combinations

addP(1,2).catch().then().then()
  1. addP(1,2) returns p1
  2. catch() returns p2 followed by p3, p4 .
  3. if p1 resolves without error then as it doesnt have a then handler attached only a catch handler attached, it skips that and just resolves the promise p2. On p2 then handler(t2) is attached so that is called and then p2 resolves according to the return value of t2.
  4. if p1 throws error then as it has a catch handler(c1) then c1 is called and after that p2 resolves/rejects depending on the return value of the c1.
addP().then().catch().then().then()
  1. addP() returns p1, p1.then() returns p2, p2.catch() returns p3, and so on.
  2. p1 is rejected then handler on p1 is skipped, p2 is rejected and the catch handler on p2(c2) is executed. after that p3 will resolve/reject based on the return value of c2.

So by now you might have quite understood how the combination of then and catch works on a promise chain.

Lets get down to implementing it.

function Deferred(){let p = new MyPromise((resolve,reject)=>{
this.resolve = resolve;
this.reject = reject;
});this.promise = p;}function MyPromise(executor){
this.state = 'initial';
this.value = undefined;
//this.onValue = [];
//this.onError = [];
this.subscriptions = [];
executor(resolve.bind(this),reject.bind(this));
}
MyPromise.prototype = {
then: function(handler){
let deferred = new Deferred();if(this.state === 'initial'){
this.subscriptions.push({deferred:deferred, handler:handler,type:'then'});
}
else{ //if(this.state === 'resolved')
sendResponse(this,{deferred:deferred, handler:handler,type:'then'});
}

return deferred.promise;
},
catch:function(handler){
let deferred = new Deferred();if(this.state === 'initial'){
this.subscriptions.push({deferred,handler, type:'catch'});
}
else {
sendResponse(this,{deferred,handler, type:'catch'});
}
return deferred.promise;
}
}//please not that below are not global function but private inside //our file
function resolve(value){
this.state = 'resolved';
this.value = value;
this.subscriptions.map(nextObj=>sendResponse(this,nextObj));
}
function reject(error){
this.state = 'rejected';
this.error = error;
this.subscriptions.map(nextObj=>sendResponse(this,nextObj));
}
function sendResponse(self,nextObj){

let state = self.state;
let expectedType = self.state === 'resolved' ?'then' : 'catch';
let response = state === 'resolved' ? self.value : self.error;
setImmediate(function(){if(nextObj.type === expectedType){let handlerValue = nextObj.handler(response);
if(handlerValue instanceof MyPromise){
handlerValue.then(function(value){
nextObj.deferred.resolve(value);
}).catch(function(error){
nextObj.deferred.reject(error);
})
}
else{
nextObj.deferred.resolve(handlerValue);
}
}else{//type is not matching expectedType
//don't call handler
if(state==='resolved')
nextObj.deferred.resolve(response);
else
nextObj.deferred.reject(response);
}
})}

So here we now maintain only one array of subscriptions(onValue and onError) and store the type along with each so that we can identify which is the one. When resolving we check if the promise has resolved and the current subscription is the then handler then we call the then handler and resolve the next promise else we just resolve the next promise without calling the then handler. Similarly in case of catch we check if the current handler is the catch handler then we call that handler and reject the next promise else just reject the next promise.

So finally we are there. We have our own version of promise which behaves like the native Promise.

Hope you enjoyed the series of posts on Promises. Please leave a comment if this helped you understand Promise better or if you have any questions. :)

--

--

Pankaj Bhageria
JavaScript Bytes

programmer, meditator, likes to teach and help,believes in keeping things simple