Implement Custom Promise from scratch using vanilla javascript

This is another most commonly asked front-end interview question in most of the top companies. I myself have asked this question many times when I interviewed people in Amazon.

This is a fairly straightforward question, to be honest, and if you are a good javascript developer, you should answer this question fairly easily.

(Reference - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)

Let's break down the problem statement:

  1. We are asked to implement a custom version of Promise class so that by doing let's say const customPromise = new CustomPromise((resolve, reject) => {});, you should be able to get the functionalities of the actual Promise.

  2. Ok, obviously in a given short time of the interview, we can't implement everything that a promise does. We can hence check beforehand what are the things the interviewer is expecting to be done. This is critical as the interviewer would expect the candidates to clarify their thought process with them regardless of whether they are correct or not. Let's say that we implement the following things of the Promise functionality:

    1. Actual Promise constructor implementation

    2. Handing .then

    3. Handling .catch

    4. Handling .finally

Let's start by building one use case at a time.

Actual Promise implementation

class CustomPromise {
  // executableMethod takes the resolve, reject arguments as its parameters.
  constructor(exectableMethod) {}
}

// Setting some defaults.
CustomPromise.prototype.state = "pending";
CustomPromise.prototype.result = undefined;

export default CustomPromise;

We have a simple structure to get started. We have the constructor that takes the method and we also set some defaults for state and result. In the actual promise, they would be represented as Internal Slots (Reference: https://v8.dev/blog/understanding-ecmascript-part-1#internal-slots-and-internal-methods)

Ok, we know that Promise constructor takes the following structure:

constructor((resolve, reject) => { }) (or)

if done using ES5, function Promise(function (resolve, reject) {}) {}

We need a way to access the arguments (resolve and reject) that are passed inside the method. It's a bit tricky as we can't do it directly. arguments keyword would only return the method and not its arguments. Bind/Call comes to our rescue.

constructor(exectableMethod) {
    if (typeof exectableMethod !== "function") {
      throw new Error("function is not provided in the arguments. exiting.");
    }

    function resolve(data) {}
    function reject(err) {}

    // This is critical as we need to bind the `resolve`
    // and the `reject` methods to our internal methods
    // for us to be able to access whenever the promise
    // is either resolved or rejected from the code by the user.
    exectableMethod.call(null, resolve, reject);
}

Let's leave this right here and move on to .then implementation.

Handling .then

We have the promise in the following structure:

const promise = new Promise((resolve, reject) => {
    ...
});

promise.then((data) => {
    // Do something with the data.
});

If you take a closer look, the .then function takes a method as an argument, let's call that as then callback. That method gets invoked whenever the promise is resolved. In order for us to invoke that method, we need to store that method in memory.

// Takes the callback methods of `.then`
thenQueue = [];
then(thenCallback) {
    this.thenQueue.push(thenCallback);
    return this; // This is important to support chaining
}
function resolve(data) {
  this.state = "fulfilled";
  this.result = data;
  let value = data;
  this.thenQueue.forEach((method) => {
    if (typeof method === "function") {
      value = method.call(null, value);
    }
  });
}

In this resolve method, we look for all the chained .then and call each of them with the value returned from the previous then. Also, we change the status of the promise to fullfilled and the result to data

Handling .catch

Handling .catch is pretty simple just like above. There is no .catch chaining so just one method call.

// Catch callback
catchCallBack = null;
function reject(err) {
  this.state = "fulfilled";
  if (typeof this.catchCallback === "function") {
    this.catchCallback.call(null, err);
  }
  handleFinally.call(this);
}
catch(catchCallback) {
    this.catchCallback = catchCallback;
    return this;
}

Handling .finally

.finally gets called regardless of whether the promise is resolve/rejected.

// Finally callback
finallyCallback = null;
function resolve(data) {
  this.state = "fulfilled";
  this.result = data;
  let value = data;
  this.thenQueue.forEach((method) => {
    if (typeof method === "function") {
      value = method.call(null, value);
    }
  });
  handleFinally(); // Add this line to call the finally
}
function reject(err) {
  this.state = "fulfilled";
  if (typeof this.catchCallBack === "function") {
    this.catchCallBack.call(null, err);
  }
  handleFinally(); // Add this line to call the finally
}
function handleFinally() {
  if (typeof this.finallyCallback === "function") {
    this.finallyCallback.call(null);
  }
}
finally(finallyCallback) {
    this.finallyCallback = finallyCallback;
}

You can find the complete source code here - https://gist.github.com/thearunkumar/2bf8288a82b0f71d9f971afb79c070b5

You can use the following snippet to test the above code:

const customPromise = new CustomPromise((res, rej) => {
  setTimeout(() => {
    res("Hey");
  }, 2000);
});
customPromise
  .then((data) => {
    console.log("First then", data);
  })
  .then((data) => {
    console.log("Second then", data);
  })
  .then((data) => {
    console.log("Third then", data);
  })
  .catch((err) => {
    console.log("Catch", err);
  })
  .finally(() => {
    console.log("Finally");
  });

If you like this article, you can check out my other articles as well and subscribe to the newsletter to get my latest article right in your inbox.

Cheers

Arunkumar Sri Sailapathi.