DEV Community

vijayprwyd
vijayprwyd

Posted on • Updated on

Polyfill for Promises

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));
Enter fullscreen mode Exit fullscreen mode

And we specify the tasks to be completed after promise resolution as :

promise.then((val) => console.log(val)).catch(err => console.log(err));
Enter fullscreen mode Exit fullscreen mode

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 and catch
  • then and catch are functions which again accepts a callback and also they can be chained . Hence both must return a reference to this
  • We need to store the reference to callback function passed to then and catch 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 the then callback . If executor rejects , we must invoke catch callback.
  • For simplicity , let us assume that our promise will always resolve. Hence for now , we will not implement our catch functionality , but both then and catch implementations are exactly identical
  • Lets store the callback passed to then in a variable named onResolve

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

Lets check the executor function which we defined initially:

let executor = (resolve, reject) => setTimeout(() => resolve(1000), 1000)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)));


Enter fullscreen mode Exit fullscreen mode

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));

Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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);
};

Enter fullscreen mode Exit fullscreen mode

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 invoke resolve .
  • If any promise is rejected we invoke the reject

The complete implementation can be found in this gist .

Github

Top comments (5)

Collapse
 
ankitg32 profile image
Ankit Gupta

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.

Collapse
 
vijayprwyd profile image
vijayprwyd

Hi Ankit, Yeah you are absolutely correct. resolve, reject and all should not be a part of the object instance. Thanks very much for pointing it out . Have updated the article and code accordingly .

Collapse
 
ankitg32 profile image
Ankit Gupta

Thanks! I'm glad I could contribute.

Collapse
 
dhirendrapratapsingh profile image
Dhiren singh • Edited

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
}
}

Collapse
 
vinod_d_7 profile image
Vinod D

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