Go to content
Blog / JavaScript /

Let's write a Promise polyfill

TL:DR; You can watch me live coding a promise’s polyfill on YouTube. Watch Let’s build a promise polyfill playlist.

We’re going to build a Promise polyfill; it’s the best way to understand how things work under the hood, after all.
My goal is not to have the most performant, or feature rich, implementation, but I’ll try to optimize for ease of readability.

A brief disclaimer before we start: you might have heard that Promise are going to disappear soon, cause async function permit to further improve how we handle asynchronous code. That’s only partially true, because async functions are built on top of Promise, having a solid understanding of how Promise works is important as never before.

So let’s start from the very beginning: What’s a Promise?
A promise represents the result of an operation (usually an asynchronous operation), it is a wrapper around a value, that may, or may not be available sometimes in the future. We stop to care about this. It’s useful because it permits developers to more easily work with a future value, as it was already available.

We create a new promise using the new operator.

new Promise((resolve, reject) => {
  resolve(42);
});

Despite being a function under the hood, class in JavaScript can’t be executed without the new operator. So it does make sense to model our shim as a class.

class Promifill {
  constructor () {}
}

A promise is characterized by its value and state.

A promise could have as value, whatever existing JavaScript value. It makes sense to consider undefined as initial value.

class Promifill {
  constructor () {
    this.value = void 0;
  }
}

Differently the state could assume only three possibile values:

  • FULFILLED, when the operation represented by the promise has been successfully completed, and its result has been used to define promise’s value.

  • REJECTED, when the operation represented by the promise has been completed, but it failed. In this case, the reason for the failure is used to define promise’s value.

  • PENDING, when the operation represented by the promise has not yet been completed.

Also, very important to remind is that once a promise is fulfilled, or rejected, there’s no way its value, and state could be further modified.
Domenic Denicola has written very clearly about the nomenclature.

const [
  FULFILLED,
  REJECTED,
  PENDING
] = [true, false, void 0];

class Promifill {
  constructor () {
    this.state = PENDING;
    this.value = void 0;
  }
}

Let’s now add to the constructor signature the executor parameter. It is a mandatory parameter, and it must be enforced as such.

constructor (executor) {
  if (typeof executor != "function") {
    throw new TypeError(`Promise resolver must be a function`);
  }
}

The executor is sinchronously executed, receiving as arguments two functions, resolve and reject, which have the power to settle the fate of the promise.

constructor (executor) {
  if (typeof executor != "function") {
    throw new TypeError(`Promise resolver must be a function`);
  }

  this.state = PENDING;
  this.value = void 0;

  executor(resolve, reject);
}

resolve takes as argument the value that should be used to define value of the promise. For now, let’s consider the state of the promise as fulfilled. That’s not correct… but we’ll fix this later.

const resolve =
  (value) => {
    this.value = value;
    this.state = FULFILLED; // #FIXME
  };

Also reject takes as input parameter the value, that should be used to define the value of the promise; for a rejected promise, the reason for the rejection is considered as value. The state in this case could be safely assumed as always rejected.

const reject =
  (reason) => {
    this.value = reason;
    this.state = REJECTED;
  };

The biggest flaw in current implementation is that it gives everybody free access to promise’s internal state and value. Native promises keep this data into internal slots.
It’s not possible to replicate internal slots in userland, but we can go pretty close.

Let’s start by replacing the instance’s fields with readonly accessors on the class prototype.

class Promifill {
  get state () {
    return PENDING;
  }

  get value () {
    return void 0;
  }

  constructor (executor) {
    if (typeof executor != "function") {
      throw new TypeError(`Promise resolver must be a function`);
    }

    const resolve =
      (value) => {
        this.value = value;
        this.state = FULFILLED; // #FIXME
      };

    const reject =
      (reason) => {
        this.value = reason;
        this.state = REJECTED;
      };

    executor(resolve, reject);
  }
}

resolve and reject are going to shadow those fields when executing, by creating instance fields, which are not enumerable, not configurable, and not writable.

Let’s create a simple utility for this job:

const defineProperty =
  (target, propName, propValue) => {
    Object.defineProperty(target, propName, { value: propValue });
  };

With this in place we can rewrite resolve and reject as:

const resolve =
  (value) => {
    defineProperty(this, "value", value);
    defineProperty(this, "state", FULFILLED); // #FIXME
  };

const reject =
  (reason) => {
    defineProperty(this, "value", reason);
    defineProperty(this, "state", REJECTED);
  };

Finally let’s also add a check to exit early in case the state of the promise has already been settled.

class Promifill {
  get state () {
    return PENDING;
  }

  get value () {
    return void 0;
  }

  get settled () {
    return false;
  }

  constructor (executor) {
    if (typeof executor != "function") {
      throw new TypeError(`Promise resolver must be a function`);
    }

    const resolve =
      (value) => {
        if (this.settled) {
          return;
        }

        defineProperty(this, "settled", true);

        defineProperty(this, "value", value);
        defineProperty(this, "state", FULFILLED); // #FIXME
      };

    const reject =
      (reason) => {
        if (this.settled) {
          return;
        }

        defineProperty(this, "settled", true);

        defineProperty(this, "value", reason);
        defineProperty(this, "state", REJECTED);
      };

    executor(resolve, reject);
  }
}

Looking at the code written so far, we can see how the executor is immediately invoked. We can’t know, nor should we care, about the kind of code we’re running… It might throw, and we should be ready for this possibility.

When executor throws an exception, we still get back a promise, that is rejected, with the error been thrown as value. Implementing this behaviour in our shim is pretty straightforward.

try {
  executor(resolve, reject);
} catch (error) {
  reject(error);
}

If we take a look at code from a certain distance, there’s a thing that certainly will stand out. I mean that #FIXME comment we’ve left earlier. It’s quite time to fix it… but just before that, what’s wrong there?

We’ve assumed so far, that reject always set promise’s state to REJECTED, but also that resolve always set promise’s state to FULFILLED; but this is not always the case. There’s an important exception indeed. In case resolve receives as input, a value, that is itself a promise, the new promise should be resolved with state, and value of this promise (once its fate is set).

So, how to know when a promise becomes fulfilled, or gets rejected? It’s possible to be somehow notified of a promise changing its state using the then method, that each promise shares on the class’ prototype.

then accepts as arguments two functions: onfulfill and onreject; the first is executed when the promise becomes fulfilled, the latter when it gets rejected. then itself returns a new promise, that is settled with the value onfulfill, or onreject will return.

class Promifill {
  constructor (executor) {}

  then (onfulfill, onreject) {
    return new this.constructor((resolve, reject) => {
      // ?
    });
  }
}

However onfulfill, and onreject are not synchronously executed after promise’s state changes. Their execution is asynchronous; the JavaScript engine schedules their execution as a microtask. This should guarantee that they’re executed asynchronously, but anyway before any other task.

Jake Archibald has a great post on this topic, and more recently he has also spoken about JavaScript event loop at JSConf.Asia 2018. I absolutely recommend these resources.

We’ll see later how to mock this behaviour in our polyfill; for now the take away is that since onfulfill, and onreject are not immediately executed, we need a place to store them for when the right moment arrives. Let’s prepare a such place:

class Promifill {
  constructor (executor) {
    if (typeof executor != "function") {
      throw new TypeError(`Promise resolver must be a function`);
    }

    defineProperty(this, "observers", []);
  }
}

So, it’s time to implement then.

then (onfulfill, onreject) {
  return new this.constructor((resolve, reject) => {
    const internalOnfulfill =
      (value) => {
        resolve(onfulfill(value));
      }

    const internalOnreject =
      (reason) => {
        resolve(onreject(reason));
      };

    this.observers.push({
      onfulfill: internalOnfulfill,
      onreject: internalOnreject
    });
  });
}

The above implementation is still incomplete, but it’s a good starting point to understand what’s going on.

We’ve wrapped the onfulfill, onreject functions (arguments of then) into another couple of functions, which capture in the closure resolve, and reject, and doing so, also the capability to resolve, reject the returned promise. These functions are then stored into observers field, so that later we can reference, and execute them.

internalOnfulfill, and internalOnreject are mostly incomplete at this point.

For instance internalOnfulfill should also put into account the fact that onfulfill may not be provided, or that it may throw an exception. So it’s better written as:

const internalOnfulfill =
  (value) => {
    try {
      resolve(
        typeof onfulfill == "function"
          ? onfulfill(value)
          : value
      );
    } catch (error) {
      reject(error);
    }
  };

And almost the same consideration could be done about internalOnreject, so that it could be rewritten as:

const internalOnreject =
  (reason) => {
    try {
      if (typeof onreject == "function") {
        resolve(onreject(reason));
      } else {
        reject(reason);
      }
    } catch (error) {
      reject(error);
    }
  };

So, let’s have a look at then in its completeness:

then (onfulfill, onreject) {
  return new this.constructor((resolve, reject) => {
    const internalOnfulfill =
      (value) => {
        try {
          resolve(
            typeof onfulfill == "function"
              ? onfulfill(value)
              : value
          );
        } catch (error) {
          reject(error);
        }
      };

    const internalOnreject =
      (reason) => {
        try {
          if (typeof onreject == "function") {
            resolve(onreject(reason));
          } else {
            reject(reason);
          }
        } catch (error) {
          reject(error);
        }
      };

    this.observers.push({
      onfulfill: internalOnfulfill,
      onreject: internalOnreject
    });
  });
}

Having completed then, we can now use it to fix resolve. Do you remember the problem?

const resolve =
  (value) => {
    if (this.settled) {
      return;
    }

    defineProperty(this, "settled", true);

    defineProperty(this, "value", value);
    defineProperty(this, "state", FULFILLED); // #FIXME
  };

We had assumed that resolve always produce a fulfilled promise, but this doesn’t always hold true: that’s not the case when the value resolve receives as argument is itself a promise.

So, first step is to determine whether a value is a promise (or better a thenable), or not. I use duck checking for this purpose:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

I consider a value to be a thenable when it has a then method attached to it. Let’s write an utility to detect this kind of objects.

const isThenable =
  (subject) => subject && typeof subject.then == "function";

Being a thenable is not a big deal on its own. We’re much more interested into distinguish unsettled thenable from whatever else. That’s cause we can easily access state, and value of promises created by our shim.

const resolve =
  (value) => {
    if (this.settled) {
      return;
    }

    defineProperty(this, "settled", true);

    const thenable = isThenable(value);

    if (thenable && value.state === PENDING) {
      // ?
    } else {
      defineProperty(this, "value",
        thenable
          ? value.value
          : value);
      defineProperty(this, "state",
        thenable
          ? value.state
          : FULFILLED);
    }
  };

So, what can we do when the thenable is not yet settled? We’re going to register an observer on it, in order to be notified when its state changes. However we can’t simply write it like this:

value.then(resolve, reject);

Otherwise the guard on this.settled, that we’ve put in place earlier to protect the value/state from unlegitimate changes, won’t permit us to proceed further. We need to put in place a mechanism to bypass that safety guard.
Sure there are different ways to make this… I’m going to setup a bypass mechanism based on a key that simply is not possible to have from outside the class’ constructor.

const secret = [];

const resolve =
  (value, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    const thenable = isThenable(value);

    if (thenable && value.state === PENDING) {
      value.then(
        (v) =>
          resolve(v, secret),
        (r) =>
          reject(r, secret)
      );
    } else {
      defineProperty(this, "value",
        thenable
          ? value.value
          : value);
      defineProperty(this, "state",
        thenable
          ? value.state
          : FULFILLED);
    }
  };

Current implementation works, but triggers then’s getter twice: first time is for the type check typeof obj.then == "function", and the second for the execution. It’s required that then’s getter gets invoked only once per use. Let’s fix this:

const resolve =
  (value, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    const then_ = value && value.then;
    const thenable = typeof then_ == "function";

    if (thenable && value.state === PENDING) {
      then_.call(
        value,
        (v) =>
          resolve(v, secret),
        (r) =>
          reject(r, secret)
      );
    } else {
      defineProperty(this, "value",
        thenable
          ? value.value
          : value);
      defineProperty(this, "state",
        thenable
          ? value.state
          : FULFILLED);
    }
  };

On the other hand, reject was already pretty fine. At the point it’s executed, is already pretty clear that we’re going to end up with a new rejected promise, so there’s no reason to wait:

const lateRejection = new Promise((_, reject) => {
  setTimeout(reject, 60000, "A minute late rejection.");
});

const promise = new Promise((resolve) => {
  resolve(lateRejection);
});

promise.catch(() => console.log("Immediate, instead!"));

It should simply be extended to recognize the secret key, when provided. So it ends up being as below:

const reject =
  (reason, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    defineProperty(this, "value", reason);
    defineProperty(this, "state", REJECTED);
  };

Having fixed the constructor, let’s recap what we’ve done till this point.

We’ve in place a reliable mechanism to mark an object representing a promise as fulfilled, or rejected. We’ve also a way to register an handler that should be invoked as microtask - that is before any other task - when promise’s fate is set.

What is still missing is something that triggers the execution of all the registered observers when a promise changes its state.

But, at this point is not hard to find where promise’s fate is defined: in the resolve, and reject functions, which are declared in the constructor. That’s the best place to schedule the observers as microtask.

const resolve =
  (value, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    const then_ = value && value.then;
    const thenable = typeof then_ == "function";

    if (thenable && value.state === PENDING) {
      then_.call(
        value,
        (v) =>
          resolve(v, secret),
        (r) =>
          reject(r, secret)
      );
    } else {
      defineProperty(this, "value",
        thenable
          ? value.value
          : value);
      defineProperty(this, "state",
        thenable
          ? value.state
          : FULFILLED);

      schedule(this.observers);
    }
  };

const reject =
  (reason, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    defineProperty(this, "value", reason);
    defineProperty(this, "state", REJECTED);

    schedule(this.observers);
  };

Let for a moment schedule not implemented. The instance’s observers field is an array of objects, each containing an onfulfill, and onreject handler… but only one of these is effectively executed, on the basis of the promise’s state. When schedule is executed the promise’s state is already known, so that we can pass to schedule the correct handler, together with the promise’s value.

schedule(
  this.observers.map((observer) => ({
    handler: this.state === FULFILLED
      ? observer.onfulfilled
      : observer.onrejected,
    value: this.value
  }))
);

Before moving on, it’s important to consider that, then may happen to be called on a promise, that is already fulfilled, or rejected. To determine whether that’s the case we can check promise’s state, and consequently registering, or scheduling the observers.

if (this.state === PENDING) {
  this.observers.push({
    onfulfill: internalOnfulfill,
    onreject: internalOnreject
  });
} else {
  schedule([{
    handler: this.state === FULFILLED
      ? internalOnfulfill
      : internalOnreject,
    value: this.value
  }]);
}

It’s quite time to implement schedule. The problem we’re going to solve could be summerized as:

How can we mock in a pre-promise environment the execution of a microtask?

Let’s consider for now only browsers as target environment of our shim. Later we’re going to extend it, to be environment-independent, but let’s keep it simple for now.

Since we’re just considering browsers, we can exploit the fact that the handler passed to a mutation observer is scheduled as a microtask.

const func =
  () => console.log("A microtask");

const observer = new MutationObserver(func);
const node = document.createTextNode("");

observer.observe(node, { characterData: true });

setTimeout(() => console.log("A task"), 0);
node.data = 1;

In the above snippet, func is scheduled as microtask when node’s data changes, and it’s invoked before the handler passed to setTimeout.

const schedule =
  (() => {
    let microtasks = [];

    const run =
      () => {
        let handler, value;
        while (microtask.length > 0 &&
          ({ handler, value } = microtasks.shift())) {
          handler(value);
        }
      };

    const observer = new MutationObserver(run);
    const node = document.createTextNode("");

    observer.observe(node, { characterData: true });

    return (observers) => {
      if (observers.length === 0) {
        return;
      }

      microtasks = microtasks.concat(observers);
      observers.length = 0;

      node.data = node.data === 1
        ? 0
        : 1;
    };
  })();

This code is not super beautiful; we’ll improve it later, anyway. Let’s look better at the while loop:

while (queue.length > 0 && ({ handler, value } = queue.shift())) {
  handler(value);
}

What do you think should happen in case handler throws an exception?

We’ve sprinkled a few try... catches around, which helps create rejected promises whenever an otherwise uncaught exception occurs. But they are somehow overprotective. In fact what would have been an uncaught exception using the native built-in, is always caught somewhere in our shim.

We’ve to re-throw the exception, immediately (not synchronously) after having marked the promise rejected.

The only condition is that the promise’s rejection remains uncaught, that is there’s at least a promise’s chain branch that is not recovered with then(*, onreject).

In order to know whether that’s the case we need to know the whole promises’ chain. So, let’s start by adding to our class another field, to track the chain.

class Promifill {
  constructor (executor) {
    // ...

    defineProperty(this, "chain", []);
    defineProperty(this, "observers", []);

    // ...
  }

  then (onfulfill, onreject) {
    const chainedPromise = new this.contructor((resolve, reject) => {
      // ...
    });

    this.chain.push(chainedPromise);
    return chainedPromise;
  }
}

Let’s now consider the following utilities, defer:

const defer =
  (handler) =>
    (...args) => {
      setTimeout(handler, 0, ...args);
    }

and, thrower:

const thrower =
  (error) => {
    throw error instanceof Error
      ? error
      : new Error(error);
  };

so that, we can write raiseUnhandledPromiseRejectionException as:

const raiseUnhandledPromiseRejectionException =
  defer((error, promise) => {
    if (promise.chain.length > 0) {
      return;
    }
    thrower(error);
  });

We can now use raiseUnhandledPromiseRejectionException to schedule the throwing of an exception when a promise gets rejected.

So it should be used in reject of course:

const reject =
  (reason, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    defineProperty(this, "value", reason);
    defineProperty(this, "state", REJECTED);

    schedule(
      this.observers.map((observer) => ({
        handler: observer.onrejected,
        value: this.value
      }))
    );

    raiseUnhandledPromiseRejectionException(this.value, this);
  };

… but also in resolve:

const resolve =
  (value, bypassKey) => {
    if (this.settled && bypassKey !== secret) {
      return;
    }

    defineProperty(this, "settled", true);

    const then_ = value && value.then;
    const thenable = typeof then_ == "function";

    if (thenable && value.state === PENDING) {
      then_.call(
        value,
        (v) =>
          resolve(v, secret),
        (r) =>
          reject(r, secret)
      );
    } else {
      defineProperty(this, "value",
        thenable
          ? value.value
          : value);
      defineProperty(this, "state",
        thenable
          ? value.state
          : FULFILLED);

      schedule(
        this.observers.map((observer) => ({
          handler: this.state === FULFILLED
            ? observer.onfulfilled
            : observer.onrejected,
          value: this.value
        }))
      );

      if (this.state === REJECTED) {
        raiseUnhandledPromiseRejectionException(this.value, this);
      }
    }
  };

Current implementation works quite well; but it has still a few problems in some edge cases, in which it ends up throwing more than what’s due. As example:

const immediateRejection = new Promise((_, reject) => {
  reject("Boom");
});

const promise = new Promise((resolve) => {
  resolve(immediateRejection);
});

When a promise is resolved to a rejected promise, there should be only one throw after the promise is resolved.

const resolve =
  (value, bypassKey) => {
    // ...

    const then_ = value && value.then;
    const thenable = typeof then_ == "function";

    if (thenable) {
      defineProperty(value, "preventThrow", true);
    }

    // ...
  };

This information is gold, and permits raiseUnhandledPromiseRejectionException to further restrict the condition on which it throws:

const raiseUnhandledPromiseRejectionException =
  defer((error, promise) => {
    if (promise.preventThrow || promise.chain.length > 0) {
      return;
    }
    thrower(error);
  });

Our scheduler works now pretty well on every browsers supporting MutationObservers; but it throws pretty fast when executed as Node.js module. That’s cause there’s no such a thing as mutation observers in node. Let’s now try to extend schedule, so that it works indifferently both on browsers, both in Node.js.

const schedule =
  (() => {
    let microtasks = [];

    const run =
      () => {
        let handler, value;
        while (microtask.length > 0 &&
          ({ handler, value } = microtasks.shift())) {
          handler(value);
        }
      };

    const observer = new MutationObserver(run);
    const node = document.createTextNode("");

    observer.observe(node, { characterData: true });

    return (observers) => {
      if (observers.length === 0) {
        return;
      }

      microtasks = microtasks.concat(observers);
      observers.length = 0;

      node.data = node.data === 1
        ? 0
        : 1;
    };
  })();

In our schedule’s implementation we can recognize two environment specific operations. Initially it setups the MutationObserver:

const observer = new MutationObserver(run);
const node = document.createTextNode("");

observer.observe(node, { characterData: true });

… and later, it triggers a mutation on the observed element:

node.data = node.data === 1
  ? 0
  : 1;

Let’s extract these operations outside our scheduler.

class MutationObserverStrategy {
  constructor (handler) {
    const node = this.node =
      document.createTextNode("");
    const observer = new MutationObserver(handler);
    observer.observe(node, { characterData: true });
  }

  trigger () {
    this.node = this.node.data === 1
      ? 0
      : 1;
  }
}

So that now the schedule can be written as:

const schedule =
  (() => {
    let microtasks = [];

    const run =
      () => {
        let handler, value;
        while (microtask.length > 0 &&
          ({ handler, value } = microtasks.shift())) {
          handler(value);
        }
      };

    const ctrl = new MutationObserverStrategy(run);

    return (observers) => {
      if (observers.length === 0) {
        return;
      }

      microtasks = microtasks.concat(observers);
      observers.length = 0;

      ctrl.trigger();
    };
  })();

Current version works exactly how the previous implementation, but it’s far easier to extend; we only need to provide a different strategy for Node.js, or for browsers which don’t support MutationObserver; and that’s exactly what we’re going to do now.

The only requirement we’ve is that all the strategies match the same unwritten interface, that is implement the trigger method.

We can use the built-in process.nextTick to schedule a microtask in Node.js. I would have preferred setImmediate, but it appears to not be completely reliable yet.

class NextTickStrategy {
  constructor (handler) {
    this.scheduleNextTick =
      () => process.nextTick(handler);
  }

  trigger () {
    this.scheduleNextTick();
  }
}

Finally in a browser that does not support MutationObserver, there’s no way I am aware of to schedule a microtask. For such cases we can still provide a strategy that mostly works, despite not being 100% spec compliant.

class BetterThanNothingStrategy {
  constructor (handler) {
    this.scheduleAsap =
      () => setTimeout(handler, 0);
  }

  trigger () {
    this.scheduleAsap();
  }
}

Now that we’ve a lib of possible strategies, what’s missing is an orchestrator, that picks the most appropriate strategy, given the current environment. Despite this sounding as a really smart move, it’s as simple as writing a function, with a couple of ifs inside.

const getStrategy =
  () => {
    if (typeof window != "undefined" &&
      typeof window.MutationObserver == "function") {
      return MutationObserverStrategy;
    }

    if (typeof process != "undefined" &&
      typeof process.nextTick == "function") {
      return NextTickStrategy;
    }

    return BetterThanNothingStrategy;
  }

At this point, making the scheduler isomorphic (or universal, or whatever it’s used these days) it’s just a matter of replacing the hardcoded use of MutationObserverStrategy with the code below:

const Strategy = getStrategy();
const ctrl = new Strategy(run);

At this point our shim works pretty well, both on browsers, both in Node.js. What’s still missing is to complete its public interface, with both the instance, and static methods.

Beyond then (that is already completed), each promise instance has two other methods, catch and finally.

catch takes as argument a function, onreject, that is invoked in case the promise on which catch is called gets rejected. It returns a new promise that is fulfilled with the value onreject will return. This description might sound familiar… and for good reasons. It’s pretty much what then(*, onreject) does. In fact we can think of catch as a shortcut for then(null, onreject), and we can also implement it as such:

class Promifill {
  constructor (executor) {
  }

  catch (onreject) {
    return this.then(null, onreject);
  }

  then (onfulfill, onreject) {
  }
}

finally is a recent addition; it is introduced with ES2018, mostly to make Promise’s public api match the historic try {} catch (error) {} finally {} block.

You might need finally in the same scenarios you would have used the finally clause of a try {} catch (error) {} block, that is when you want the same instruction to be executed indipendently from the fact the code has thrown an exception or not.

finally receives as input parameter a function, oncomplete, that is executed when the promise on which it’s called becomes fulfilled, or gets rejected. oncomplete does not receive anything as input parameter, neither its return value is used for something; in fact finally returns a new promise, that has the exact same state, and value of the promise on which finally has been called. The only case in which finally can produce a promise with different state, and value is when oncomplete throws an exception.

So let’s implement this behaviour:

class Promifill {
  constructor (executor) {
  }

  catch (onreject) {
  }

  finally (oncomplete) {
    const chainedPromise = new this.contructor((resolve, reject) => {
      const internalOncomplete =
        () => {
          try {
            oncomplete();
            if (this.state === FULFILLED) {
              resolve(this.value);
            } else {
              reject(this.value);
            }
          } catch (error) {
            reject(error);
          }
        };

        if (this.state === PENDING) {
          this.observers.push({
            onfulfill: internalOncomplete,
            onreject: internalOncomplete
          });
        } else {
          schedule([{
            handler: internalOncomplete
          }]);
        }
    });

    this.chain.push(chainedPromise);
    return chainedPromise;
  }

  then (onfulfill, onreject) {
  }
}

To complete our implementation of Promise built-in at this point we’re only missing a couple of static methods.

Promise.resolve, and Promise.reject despite some important difference are quite similar, at least for what concern the scope of usability. They are pretty useful in case you’re not sure about a value you need to work with, and want to be sure it’s a promise, or in case you know it’s a promise built with a different library, and want to cast it to a genuine Promifill promise.

Let’s talk about Promise.resolve. In case the provided value is already an original Promifill’s promise it returns it unchanged. Otherwise it creates a new promise that is resolved with the provided value, following - in case it’s a thenable - its then method.

class Promifill {
  static resolve (value) {
    return value.constructor === Promifill
      ? value
      : new Promifill((resolve) => {
        resolve(value);
      });
  }
}

Since we’ve implemented Promise.resolve on the basis of our internal resolve function, we’ve got for free the correct behaviour in case the value passed to Promise.resolve is a generic thenable.

Promise.reject creates always a new promise that is immediately rejected with the given reason. So it’s pretty straightforward to implement.

class Promifill {
  static reject (reason) {
    return new Promifill((_, reject) => {
      reject(reason);
    });
  }
}

Finally the other two functions Promise.all, and Promise.race are more the kind of utilities you could find in other libraries.

Promise.all receives as input an iterable, such as an array, and returns a new promise that is fulfilled only when all the values in the iterable are fulfilled. In this case the value of the promise is an array containing the fulfilled values of each element of the iterable. In case at least one of the element in the iterable gets rejected, the new promise is rejected as well, with the reason of the first element in the iterable been rejected.

Let’s implement this behaviour.

We start by validating the input parameters: it’s required that it is an iterable. Let’s create a couple of utility for this task; let’s have an isIterable:

const isIterable =
  (subject) => subject != null &&
    typeof subject[Symbol.iterator] == "function";

that’s used by validateIterable:

const validateIterable =
  (subject) => {
    if (isIterable(subject)) {
      return;
    }

    throw new TypeError(`Cannot read property 'Symbol(Symbol.iterator)' of ${Object.prototype.toString.call(subject)}.`);
  };

We can now use validateIterable to validate the input parameter Promise.all receives.

class Promifill {
  static all (iterable) {
    return new Promifill((resolve, reject) => {
      validateIterable(iterable);

      // ?
    });
  }
}

To implement Promise.all behaviour, we’ll need an array where to store the fulfilled value of all the element in the iterable. It starts empty, and we’ll add element to it as soon as each one become fulfilled.

So we start iterating on all the element in the iterable. We use Promifill.resolve to normalize each entry to a genuine Promifill object.

class Promifill {
  static all (iterable) {
    return new Promifill((resolve, reject) => {
      validateIterable(iterable);

      let iterableSize = 0;
      const values = [];

      for (let item of iterable) {
        ((entry, index) => {
          Promifill.resolve(entry)
            .then(
              (value) =>
                add(value, index),
              reject
            );
        })(item, iterableSize++);
      }
    });
  }
}

The internal add method should also check that all the elements in the iterable have been fulfilled, hence resolve the new promise.

const add =
  (value, index) => {
    values[index] = value;
    if (values.length === iterableSize) {
      resolve(values);
    }
  };

Current add’s implementation might result bugged; in fact in case the last element in the iterable is not the last element to be fulfilled/rejected values.length === iterableSize results true (values becomes a sparse array) and the promise becomes fulfilled too early. That’s could be fixed by remembering that Array#filter skips array’s hole; so that the check becomes something like:

const add =
  (value, index) => {
    values[index] = value;
    if (values.filter(() => true).length === iterableSize) {
      resolve(values);
    }
  };

To complete Promise.all we need to cover a last edge case. What would happen in case the iterable is empty? Current implementation returns a promise that never gets settled; but that’s not what it’s supposed to do. In this case it should return a promise that’s fulfilled, and has as value an empty array.

So first task is to determine whether an iterable is empty. So let’s add isEmptyIterable in our lib:

const isEmptyIterable =
  (subject) => {
    for (let _ of subject) {
      return false;
    }
    return true;
  };

So that in the end we can write our complete Promise.all as:

class Promifill {
  static all (iterable) {
    return new Promifill((resolve, reject) => {
      validateIterable(iterable);

      let iterableSize = 0;
      const values = [];

      if (isEmptyIterable(iterable)) {
        return resolve(values);
      }

      const add =
        (value, index) => {
          values[index] = value;
          if (values.filter(() => true).length === iterableSize) {
            resolve(values);
          }
        };

      for (let item of iterable) {
        ((entry, index) => {
          Promifill.resolve(entry)
            .then(
              (value) =>
                add(value, index),
              reject
            );
        })(item, iterableSize++);
      }
    });
  }
}

Finally Promise.race. It accepts an iterable as input parameter, and returns a new promise, that is fulfilled, or gets rejected as soon the first element in the iterable is fulfilled, or gets rejected with that value, or reason.

Strange enough, in case it receives an empty iterable, the promise it returns, never gets settled.

class Promifill {
  static race (iterable) {
    return new Promifill((resolve, reject) => {
      validateIterable(iterable);

      if (isEmptyIterable(iterable)) {
        return;
      }

      for (let entry of iterable) {
        Promifill.resolve(entry)
          .then(resolve, reject);
      }
    });
  }
}