Please go through the MDN docs for understanding about Promises
Also please note that, don't re-invent the wheel and attempt writing polyfills from scratch by ourselves for a feature which already exists. This is just an illustration of how promise likely works behind the scenes and to imagine ourselves for more understanding.
A sample promise initialisation looks like :
let promise = new Promise((resolve, reject) => setTimeout(() => resolve(1000), 1000));
And we specify the tasks to be completed after promise resolution as :
promise.then((val) => console.log(val)).catch(err => console.log(err));
Let us implement our polyfill (say PromisePolyFill in multiple steps.
From the above codes we know following :
- The promise constructor function must accept a callback as an argument. We will call it as
executor. - It must return an object with at-least two properties ,
thenandcatch -
thenandcatchare functions which again accepts a callback and also they can be chained . Hence both must return a reference tothis - We need to store the reference to callback function passed to
thenandcatchsomewhere so that they should be executed at a later point of time , depending on the status of executor. If executor resolved we must invoke thethencallback . If executor rejects , we must invokecatchcallback. - For simplicity , let us assume that our promise will always
resolve. Hence for now , we will not implement ourcatchfunctionality , but boththenandcatchimplementations are exactly identical - Lets store the callback passed to
thenin a variable namedonResolve
So our initial code looks like :
function PromisePolyFill(executor) {
let onResolve;
this.then = function(callback) {
// TODO: Complete the impl
onResolve = callback;
return this;
};
this.catch = function(callback) {
// TODO: We are ignoring this part for simplicity , but its implementation is similar to then
return this;
}
}
Lets check the executor function which we defined initially:
let executor = (resolve, reject) => setTimeout(() => resolve(1000), 1000)
This is the callback passed to our promise that we need to execute . Hence we must invoke this executor function which will accept two arguments, resolve and reject.
executor(resolve) // reject scenarios ignored for simplicity
The executor will either invoke resolve or reject depending on the status of async operation . For simplicity , we have only considered resolve function here and assume that for now our promise is always resolved.
We now need to define our resolve callback function that is passed as an argument to the executor. Our resolve function is nothing, but just triggers the callback passed to then , which we have stored in onResolve variable
function resolve(val) {
onResolve(val);
}
We have completed the initial part, of the polyfill.
So as of now our current function looks like this and works perfectly for our base happy-path scenario. We can complete our catch functionality similarly.
function PromisePolyFill(executor) {
let onResolve;
function resolve(val) {
onResolve(val);
}
this.then = function(callback) {
// TODO: Complete the impl
onResolve = callback;
return this;
};
this.catch = function(callback) {
// TODO: Complete the impl
return this;
}
executor(resolve);
}
// Sample code for test :
new PromisePolyFill((resolve) => setTimeout(() => resolve(1000), 1000)).then(val => console.log(val));
Part 2
But we have handled only the case where our executor function completed the operation at a later point of time. Lets assume that executor function is synchronous ,
new PromisePolyFill((resolve) => resolve(1000)).then(val => console.log(val));
We are likely to encounter this scenario if we directly resolve a variable without any async tasks like fetch , setTimeout etc
When we invoke our PromisePolyFill as above we get an error :
TypeError: onResolve is not a function
This happens because our executor invocation is completed even before we assign the value of then callback to our onResolve variable.
So in this case it's not possible for us to execute onResolve callback from our resolve function . Instead the callback passed to then needs to be executed somewhere else.
Now we require two more additional variables :
fulfilled : Boolean indicating if the executor has been resolved or not
called: boolean indicating if the then callback has been called or not .
Now our modified implementation looks like :
function PromisePolyFill(executor) {
let onResolve;
let fulfilled = false,
called = false,
value;
function resolve(val) {
fulfilled = true;
value = val;
if(typeof onResolve === 'function') {
onResolve(val);
called = true; // indicates then callback has been called
}
}
this.then = function(callback) {
// TODO: Complete the impl
onResolve = callback;
return this;
};
this.catch = function(callback) {
// TODO: Complete the impl
return this;
}
executor(resolve);
}
//new PromisePolyFill((resolve) => setTimeout(() => resolve(1000), 0)).then(val => console.log(val));
new PromisePolyFill((resolve) => Promise.resolve(resolve(1000)));
This eliminates out TypeError , but we still haven't executed our onResolve method.
We should do this from out this.then initialiser conditionally, if our callback is not called yet and the promise has been fulfilled :
function PromisePolyFill(executor) {
let onResolve;
let fulfilled = false,
called = false,
value;
function resolve(val) {
fulfilled = true;
value = val;
if (typeof onResolve === "function") {
onResolve(val);
called = true;
}
}
this.then = function (callback) {
onResolve = callback;
if (fulfilled && !called) {
called = true;
onResolve(value);
}
return this;
};
this.catch = function (callback) {
// TODO: Complete the impl
return this;
};
executor(resolve);
}
//new PromisePolyFill((resolve) => setTimeout(() => resolve(1000), 0)).then(val => console.log(val));
new PromisePolyFill((resolve) => resolve(1000)).then(val => console.log(val));
With same implementation we can complete our catch code as well. We will have onReject callback and rejected boolean . Its left out as an exercise :)
Part 3 :
Now we shall implement PromisePolyFill.resolve, PromisePolyFill.reject and PromisePolyFill.all just like our Promise.resolve, Promise.reject and Promise.all
resovle and reject are very straight forward. Here we return a PromisePolyFill object but pass our own executor function which we force to resolve / reject
PromisePolyFill.resolve = (val) =>
new PromisePolyFill(function executor(resolve, _reject) {
resolve(val);
});
PromisePolyFill.reject = (reason) =>
new PromisePolyFill(function executor(resolve, reject) {
reject(reason);
});
Now lets implement Promise.all.
It takes an iterable of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises.
PromisePolyFill.all = (promises) => {
let fulfilledPromises = [],
result = [];
function executor(resolve, reject) {
promises.forEach((promise, index) =>
promise
.then((val) => {
fulfilledPromises.push(true);
result[index] = val;
if (fulfilledPromises.length === promises.length) {
return resolve(result);
}
})
.catch((error) => {
return reject(error);
})
);
}
return new PromisePolyFill(executor);
};
Here again we create our own executor function, and return back our promise object which would take in this executor.
Our executor function would work as below :
- We maintain an array named
fulfilledPromisesand push values to it whenever any promise is resolved. - If all promises are resolved (
fulfilledPromises.length === promises.length) we invokeresolve. - If any promise is rejected we invoke the
reject
The complete implementation can be found in this gist .
Latest comments (5)
Promise resolution callback either success/failure will be executed asynchronously.
Where as above polyfill resolve/reject gets executed immediately.
console.log('Hello');
new PromisePolyFill((resolve) => resolve(1000)).then(val => console.log(val));
console.log('There');
//output to console as below
Hello
1000
There
where as Promise will print as below
console.log('Hi1');
new Promise((resolve) => resolve(1000)).then(val => console.log(val));
console.log('There1');
//output to console as below
Hello1
1000
There1
This was helpful. Thanks Vijay . One minor suggestion Vijay. Since we know that either reject() or rseolve() cab run only once. Any futherr call for either of them are not entertained. We add these flags (!fulfilled) to the conditions
function resolve(val) {
fulfilled = true;
value = val;
if(typeof onResolve === 'function' && !fulfilled) {
//reject or resolve can happen only once
onResolve(val);
called = true; // indicates then callback has been called
}
}
Great explanation, Vijay!
My two cents: since .resolve .reject and .all are static methods, I don't quite understand why are we adding them onto the object instance as well (this.resolve, this.reject, and this.all). Should these be not put outside the custom promise and implemented directly onto the promise object?
For ex:
myPromise.resolve = val => new myPromise(resolve => resolve(val));
Would love to hear your thoughts on this.
Hi Ankit, Yeah you are absolutely correct.
resolve,rejectandallshould not be a part of the object instance. Thanks very much for pointing it out . Have updated the article and code accordingly .Thanks! I'm glad I could contribute.