Abstract:

This post is about my understanding of JavaScript Promise and shows example of my own Promise class in order to help understand Promise in details.

Damn, Callback Hell

JavaScript is well known as a single-thread scripting language. However, there are some tasks that need to wait for responses to come back for a period. We do not want to block other code execution during the waiting period.

How to avoid this kind of situation? JavaScript supports the asynchronous mechanism to let async tasks, for example, ajax calls, execute when their responses come back without blocking synchronous code logic.

To handle a response in an async task, a common way is to use a callback.

A callback is a function passed as an argument to another function

https://www.w3schools.com/js/js_callback.asp

The callback function is executed when a response comes back. Then depending on the status of the response, the callback function runs different logic.

Now, consider a situation in which

you have a sequence of tasks, and each task needs to wait for the previous task to complete. For example, I have tasks

task_1 -> task_2 -> task_3 -> ........ -> task_n

In this situation, if you choose callbacks to handle the above tasks’ chain, what will your code look like? It’s probably going to be like this.

The above code piece is the so-called “Callback Hell”. With almost infinite nested, nested, ……, and nested callbacks inside callbacks, it makes code readability become almost none.

OK Promise, no more Callback Hell

A promise is a JavaScript object that is built with functionalities to handle a sequence of async tasks with the chaining mechanism, instead of leading to callback hells.

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
}).then(function (result) { // (**)
  alert(result); // 1
  return result * 2;
}).then(function (result) { // (***)
  alert(result); // 2
  return result * 2;
}).then(function (result) {
  alert(result); // 4
  return result * 2;
});

The chaining mechanism really saves us from reading those infinite callback hells. It provides a way for better readability and is also easier to understand.

To understand more details about Promise and its concepts, you can read through Promises/A+ Documentation and Promise doc on MDN. I am not going to discuss the concept details here.

My Promise Polyfill

Promise is a built-in object in all of today’s JavaScript engines (assuming IE is not taken into consideration). I can easily use Promise in vanilla JS by creating a Promise object, for example Promise.resolve(myValue) . Wait, wait, so what happens under the scene when I use code like this?

new Promise((resolve, reject) => {
  // resolve and reject logic
}).then((result) => {
  // result handling
});

What is the inside implementation of Promise object?

Here I would like to show how Promise works by implementing a simple Promise myself.

First, by looking at the way an Promise object is initiated

const myPromise = new Promise((resolve, reject) => {/*custom logic*/})

It shows constructor of Promise class should take in a callback function, and at either fulfilled or rejected state, the Promise should run resolve or reject logic by calling either of the function.

The following shows the skeleton of how my Promise polyfill looks like

const state = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
};

class MyPromise {
  constructor(executor) {
    // initial state
    this._state = state.PENDING;
    this._value = undefined;
    this._error = undefined;

    // a list to save promise chains before promise is settled
    this._thenQueue = [];

    const resolve = this._onFulfilled.bind(this);
    const reject = this._onRejected.bind(this);
    executor(resolve, reject);
  }

  /** start: pseudo internal methods */
  _onFulfilled(value) {}

  _onRejected(error) {}
  /** end: pseudo internal methods */

  /** start: exposed methods */
  then(fulfillFn, rejectFn) {}

  catch(rejectFn) {}
  /** end: exposed methods */
}

Here are some points that need to pay attention to in the above piece of code.

Promise State

According to Promise/A+ standard, a Promise object must be in one of pendingfulfilled, or rejected states. And, a pending promise can either be fulfilled with a value or rejected with an error. Therefore, in the above code

  • _state: saves the current state of promise. value = pending | fulfilled | rejected
  • _value: if fulfilled, save fulfilled value to pass down to promise chain
  • _error: if rejected, save error reason

Save Promise Chain — _thenQueue

Promise is chainable, so _thenQueue is used to cache every promise object in the chain.

Methods to handle fulfilled or rejected state

The executor function that passed into MyPromise.constructor contains custom logic about under what condition should the promise be fulfilled or rejectedexecutor function takes in 2 args resolve and reject which should be internal handler function from promise object.

Now, I have the skeleton of my Promise polyfill, next I am going to show the whole code about how my promise work when it comes to fulfilled or rejected state, also including chaining

Thanks a lot to YouTube video “Promises From Scratch in A Post-Apocalyptic Feature”. It helped me a lot to understand implementation of Promise. Good stuff to check out: https://youtu.be/4GpwM8FmVgQ

https://youtu.be/4GpwM8FmVgQ
const state = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
};

const isThenable = maybePromise => maybePromise && (typeof maybePromise === 'function');

class MyPromise {
  constructor(executor) {
    this._state = state.PENDING;
    this._value = undefined;
    this._errReason = undefined;

    this._thenQueue = [];

    if (typeof executor === 'function') {
      setTimeout(() => {
        try {
          executor(
            this._onFulfilled.bind(this),
            this._onRejected.bind(this)
          );
        } catch (error) {
          this._onRejected(error);
        }
      });
    }
  }
  
  /** start external methods */
  then(onFulfilled, onRejected) {
    const controlledPromise = new MyPromise(onFulfilled, onRejected);
    this._thenQueue.push([controlledPromise, onFulfilled, onRejected]);

    if (this._state === state.FULFILLED) {
      this._propagateFulfilled();
    } else if (this._state === state.REJECTED) {
      this._propagateRejected();
    }
    return controlledPromise;
  }
  
  catch(onRejected) {
    return this.then(undefined, onRejected);
  }

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }
  
  static reject(value) {
    return new MyPromise((_, reject) => reject(value));
  }
  /** end external methods */

  /** start pseudo internal methods */
  _onFulfilled(value) {
    if (this._state === state.PENDING) {
      this._state = state.FULFILLED;
      this._value = value;
      this._propagateFulfilled();
    }
  }

  _onRejected(error) {
    if (this._state === state.REJECTED) {
      this._state = state.REJECTED;
      this._errReason = error;
      this._propagateRejected();
    }
  }

  _propagateFulfilled() {
    this._thenQueue.forEach(([controlledPromise, onFulfilled]) => {
      if (typeof onFulfilled === 'function') {
        const valueOrPromise = onFulfilled(this._value);

        if (isThenable(valueOrPromise)) {
          valueOrPromise.then(
            value => controlledPromise._onFulfilled(value),
            errorReason => controlledPromise._onRejected(errorReason)
          );
        } else {
          controlledPromise._onFulfilled(valueOrPromise);
        }
      } else {
        return controlledPromise._onFulfilled(this._value);
      }
    });
    this._thenQueue = [];
  }

  _propagateRejected() {
    this._thenQueue.forEach(([controlledPromise, _, onRejected]) => {
      if (typeof onRejected === 'function') {
        const valueOrPromise = onRejected(this._errReason);

        if (isThenable(valueOrPromise)) {
          valueOrPromise.then(
            value => controlledPromise._onFulfilled(value),
            errorReason => controlledPromise._onRejected(errorReason)
          );
        } else {
          controlledPromise._onFulfilled(valueOrPromise);
        }
      } else {
        return controlledPromise._onRejected(this._errReason);
      }
    });
  }
  /** end pseudo internal methods */
}

Wow, right now the logic is a lot more complicated than the simple promise skeleton. Here is something I feel worth paying attention to:

setTimeout in constructor

I wrap the executor function calling in a setTimeout in order to execute it in async way. Callback of setTimeout got added to a message queue. When JavaScript event loop does not detect any function execution in the call stack, the callback will be polled from message queue, and got executed.

Using JS microtasks is another way to achieve the same behavior. You can checkout queueMicrotask() on MDN.

isThenable check

I also learned this from Promises/A+ standard.

“thenable” is an object or function that defines a then method.

Considering a promise object contains a chain of promises, and once I am in the middle of a promise chain. My input is the return value from the previous promise. If this input is thenable , then means I need to try resolving the input promise to get output to pass down to the next promise in the chain.

Thus, I am checking if (isThenable(valueOrPromise)) to determine if I need to use a promise to wrap valueOrPromise to get my result to pass down to next item in the chain.

Idea of _propagateFulfilled and _propagateRejected

I learned this from youtube video “Promises From Scratch in A Post-Apocalyptic Feature”, it’s very inspiring to me. Basic idea of these 2 functions are executing promise chain items. While I am at one promise item in the middle of the chain, input value from last promise item has 3 possibilities: thenablevalue, or error . value / error are straightforward to handle, use _onFulfilled / _onRejected . If input is thenable, I am wrapping controlledPromise with thenable inorder to pass down current promise results.

Conclusion

Overall, to me polyfill my own promise is still challenging, I got lost a lot while trying to understand the mechanism of promise chains. I have put all the learning resources in the Reference section, and all of those helped me a lot to learn the underneath world of JavaScript Promise . Hope this could be helpful to you as well.

Reference

JavaScript Callbacks — W3School

Callback Hell and How to Rescue it?

Promises/A+ Documentation

JavaScript Promises — Understand JavaScript Promises by Building a Simple Promise Example

Promises From Scratch In A Post-Apocalyptic Future