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 ,
then
andcatch
-
then
andcatch
are 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
then
andcatch
somewhere so that they should be executed at a later point of time , depending on the status of executor. If executor resolved we must invoke thethen
callback . If executor rejects , we must invokecatch
callback. - For simplicity , let us assume that our promise will always
resolve
. Hence for now , we will not implement ourcatch
functionality , but boththen
andcatch
implementations are exactly identical - Lets store the callback passed to
then
in 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
fulfilledPromises
and 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 .
Top comments (5)
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
,reject
andall
should 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.
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
}
}
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