Promise - under the hood

I have decided to start back with how it works series of articles. Here is the first of many to come. I am pretty sure you are using/used the promise in Javascript many times, either knowingly or not. Let us see what is a Promise and how it works under the hood by writing a custom Promise function.

In Javascript, a Promise is an Object that is used to represent the eventual completion/failure of an asynchronous operation.

How does promise works?

Promise example code:

We will start with a simple example of a promise.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 5000);
});

myPromise
  .then((result) => {
    console.log(result); // "1" after 5 seconds
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log('Finally'); // Logs when promise is settled
  });

The above code snippet is a simple example that is self-explanatory if not let me explain. After 5 seconds, the above code will resolve the myPromise constructor. and it will log 1 in the console and also it will log Finally. Simple right, I can resolve a promise with resolve callback and reject a promise with reject callback and finally is called when the promise is settled.

We are going to understand and write CustomPromise function in 4 steps.

Step 1:

  1. We are going to create a file and name it as customPromise.js.
  2. let us name the function as CustomPromise which takes a callback as a parameter.
  3. To call .then(), .catch() and .finally() methods, we need to add then, catch and finally methods in this keyword of function and each method should return this to make it chainable.
  4. And invoke CustomPromise with new keyword so that it makes it chainable.

Example: customPromise.js file

function CustomPromise(callback) {
  this.then = () => this;
  this.catch = () => this;
  this.finally = () => this;
}

const callbackFn = () => {};
const myPromise = new CustomPromise(callbackFn);

myPromise
  .then(() => console.log)
  .catch(() => console.log)
  .finally(() => console.log);

Wow, we just learned how to create a CustomPromise function and how to make it chainable like an actual Promise object in Javascript.

Step 2:

  1. Promise allows us to attach as many .then() as possible. So we will create a internal array to store the callbacks passed to then() method called thenCallbacksArr.
  2. Also I should be able to attach .catch() method. So we will also store the callback in a internal variable called catchCallback.
  3. Lets create a resolve and reject callback function and pass it to the callback parameter of CustomPromise function.
function CustomPromise(callback) {
  let thenCallbacksArr = []; // To hold as many then as possible

  let catchCallback = null; // To hold catch callback

  this.then = (myThenCallback) => {
    thenCallbacksArr.push(myThenCallback); // Stores all then callbacks
    return this;
  };

  this.catch = (myCatchCallback) => {
    catchCallback = myCatchCallback;
    return this;
  };

  this.finally = () => {
    return this;
  };

  function resolve() {}

  function reject() {}

  callback(resolve, reject);
}
Points to remember:
  1. Promise only calls the first catch() method and discards the other catch methods.
  2. finally() method doesn't accept any parameters.

Step 3:

  1. Create a variable called promiseState with the initial value as pending.
  2. Create another Boolean variable as called to track whether a resolve or reject callback is called or not.
  3. When resolve or reject callback is called, pass the then or catch error argument accordingly.
  4. Once the promise is settled, Check promiseState and called variables before calling finally() method.
function CustomPromise(callback) {
  let thenCallbacksArr = []; // To hold as many then as possible

  let catchCallback = null; // To hold catch callback

  let promiseState = 'pending'; // To keep track of promise state (resolved/rejected)

  let called = false; // To know if resolve/reject is called or not

  this.then = (myThenCallback) => {
    thenCallbacksArr.push(myThenCallback); // Stores all then callbacks
    return this;
  };

  this.catch = (myCatchCallback) => {
    catchCallback = myCatchCallback;
    return this;
  };

  this.finally = (myFinallyCallback) => {
    // Post resolve or reject, we will call the finally method
    if (called && promiseState !== 'pending') {
      myFinallyCallback();
    }

    return this;
  };

  function resolve(arg) {
    promiseState = 'resolved';

    // If resolve/reject not called
    if (!called) {
      called = true;

      // We need to iterate thenCallbackArr and pass prev result to then callback
      thenCallbacksArr.reduce((result, cb) => cb(result), arg);
    }
  }

  function reject(error) {
    promiseState = 'rejected';

    // If reject/resolve not called
    if (!called) {
      called = true;

      // Catch is optionally so check if its present
      if (typeof catchCallback === 'function') {
        catchCallback(error);
      }
    }
  }

  callback(resolve, reject);
}

Above CustomPromise function will work just fine but when a promise is resolved/rejected before we can assign a then or catch or finally method, there is no code to handle that. That's what we will do in step 4.

Step 4:

  1. We need to check promiseState, called variable, and call the thenCallbacksArr which contains all the then callbacks.
  2. Also we need to push the finally callback to thenCallbackArr variable, for it to call when a promise is settled before attaching the finally method.
function CustomPromise(callback) {
  let thenCallbacksArr = []; // To hold as many then as possible

  let catchCallback = null; // To hold catch callback

  let promiseState = 'pending'; // To keep track of promise state (resolved/rejected)

  let called = false; // To know if resolve/reject is called or not

  let resolveArguments = null; // To hold resolve arguments

  let rejectArguments = null; // To hold reject error

  this.then = (myThenCallback) => {
    thenCallbacksArr.push(myThenCallback); // Stores all then callbacks

    //  If resolve callback method is already called, but then was not atached
    if (called && promiseState === 'resolved') {
      // Since there can ba many then's, call the first and store the returned result
      // and pass resolveArguments to it
      var firstThenFunzInArr = thenCallbacksArr.shift();
      resolveArguments = firstThenFunzInArr(resolveArguments);
    }

    return this;
  };

  this.catch = (myCatchCallback) => {
    catchCallback = myCatchCallback;

    //  If reject callback method is already called, but catch was not atached
    if (called && promiseState === 'rejected') {
      catchCallback(rejectArguments);
    }

    return this;
  };

  this.finally = (myFinallyCallback) => {
    thenCallbacksArr.push(myFinallyCallback);

    // Post resolve or reject, we will call the finally method
    if (called && promiseState !== 'pending') {
      myFinallyCallback();
    }

    return this;
  };

  function resolve(arg) {
    promiseState = 'resolved';

    // If resolve/reject not called
    if (!called) {
      called = true;
      resolveArguments = arg;

      // We need to iterate thenCallbackArr and pass prev result to then callback
      thenCallbacksArr.reduce((result, cb) => cb(result), arg);
    }
  }

  function reject(error) {
    promiseState = 'rejected';

    // If reject/resolve not called
    if (!called) {
      called = true;
      rejectArguments = error;

      // Catch is optionally so check if its present
      if (typeof catchCallback === 'function') {
        catchCallback(error);
      }
    }
  }

  callback(resolve, reject);
}

Demo:


Final thoughts

If you followed through with the code, you would have understood now how a promise works internally. Also, this is purely for educational purposes only, and dont ask questions like "Write a polyfill for the promise" in interviews.

I would have also missed a few edge cases. But you get it right? How it works. Hope you liked this post.

If you did, do share it with your friends and colleagues.

Join 309+ Readers

Sent out once every month. No spam 🤞